diff --git a/.config/travis/add-on.yml b/.config/travis/add-on.yml deleted file mode 100644 index 74e475dd46..0000000000 --- a/.config/travis/add-on.yml +++ /dev/null @@ -1,25 +0,0 @@ -# -# This travis config file is intended to be used by LifterLMS Add-ons. -# -# Example usage in .travis.yml: -# -# import: -# - gocodebox/lifterlms:.config/travis/add-on.yml -# - -# Import main configs. -import: - - gocodebox/lifterlms:.config/travis/main.yml - -# If $LLMS_BRANCH is specified, install the plugin from git. -install: - - | - if [ ! -z "$LLMS_BRANCH" ]; then - ./vendor/bin/llms-tests plugin https://github.com/gocodebox/lifterlms.git@${LLMS_BRANCH} - fi - -# Test against the "nightly" dev branch of the the LifterLMS core. -jobs: - include: - - php: "8.0" - env: LLMS_BRANCH=dev WP_VERSION=latest diff --git a/.config/travis/e2e.yml b/.config/travis/e2e.yml deleted file mode 100644 index 39e269b8cb..0000000000 --- a/.config/travis/e2e.yml +++ /dev/null @@ -1,20 +0,0 @@ -addons: - artifacts: - paths: - - ./tmp/e2e-screenshots - -services: - - xvfb - - docker - -jobs: - allow_failures: - - php: "8.0" - env: WP_VERSION=nightly LLMS_TRAVIS_TESTS=E2E - - include: - - php: "8.0" - env: WP_VERSION=latest LLMS_TRAVIS_TESTS=E2E - - php: "8.0" - env: WP_VERSION=nightly LLMS_TRAVIS_TESTS=E2E - diff --git a/.config/travis/eslint.yml b/.config/travis/eslint.yml deleted file mode 100644 index 1fbeaf66da..0000000000 --- a/.config/travis/eslint.yml +++ /dev/null @@ -1,23 +0,0 @@ -# -# TravisCI config file partial for running an eslint job -# -# This partial is intended to be used alongside the main.yml config found within this same directory. -# -# Example usage in .travis.yml: -# -# import: -# - gocodebox/lifterlms:.config/travis/main.yml -# - gocodebox/lifterlms:.config/travis/eslint.yml -# - -jobs: - include: - - env: ESLINT=1 - language: node_js - node_js: lts/* - before_install: - install: - - npm ci - script: - - npm run lint:js - after_script: diff --git a/.config/travis/main.yml b/.config/travis/main.yml deleted file mode 100644 index 55acdeae12..0000000000 --- a/.config/travis/main.yml +++ /dev/null @@ -1,129 +0,0 @@ -os: linux -dist: bionic -language: php - -services: - - mysql - -cache: - directories: - - node_modules - - vendor - - $HOME/.composer/cache - -env: - global: - - TESTS_DB_HOST=localhost - - TESTS_DB_NAME=llms_tests - - TESTS_DB_PASS="" - jobs: - - WP_VERSION=latest # 5.8 - - WP_VERSION="5.7" - - WP_VERSION="5.6" - - WP_VERSION="5.5" - - WP_VERSION="5.4" - -php: - - "8.0" - - "7.4" - - "7.3" - -jobs: - fast_finish: true - - allow_failures: - - env: WP_VERSION=nightly - - env: WP_VERSION=latest RUN_CODE_COVERAGE=1 - - php: nightly - - exclude: - # These WP Versions don't work on PHP 8.0 - - php: "8.0" - env: WP_VERSION="5.5" - - php: "8.0" - env: WP_VERSION="5.4" - - include: - - php: "8.0" - env: PHPCS=1 - - php: nightly - env: WP_VERSION=latest - - php: "8.0" - env: WP_VERSION=nightly - - php: "7.4" - env: WP_VERSION=latest RUN_CODE_COVERAGE=1 - before_script: - # Download CodeClimate Test Reporter - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - script: - - ./cc-test-reporter before-build - - composer run-script tests-run -- --coverage-clover clover.xml - after_script: - - ./cc-test-reporter after-build --coverage-input-type clover --exit-code $TRAVIS_TEST_RESULT - -before_install: - # Disable xDebug for faster builds - - | - if [ "1" != $RUN_CODE_COVERAGE ] && [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then - phpenv config-rm xdebug.ini - fi - # Raise PHP memory limit to 2048MB - - echo 'memory_limit = 2048M' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - # Install composer deps. - - | - if [ "8" != $( php -r "echo PHP_MAJOR_VERSION;" ) ]; then - composer install - else - composer run install-php8 - fi - -install: - - | - if [ "E2E" = "$LLMS_TRAVIS_TESTS" ]; then - sudo rm /usr/local/bin/docker-compose - curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin - nvm install --lts - npm ci - [[ -n $DOCKER_USERNAME ]] && [[ -n $DOCKER_PASSWORD ]] && echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - composer run env up - composer run env:setup - if [ "latest" != $WP_VERSION ]; then - ./vendor/bin/llms-env version $WP_VERSION - fi; - WP_VERSION_REAL=$( ./vendor/bin/llms-env wp core version ) - echo $WP_VERSION_REAL - elif [ "1" = "$PHPCS" ]; then - echo "Nothing to install" - else - composer run tests-install - fi - -script: - - | - if [ "E2E" = "$LLMS_TRAVIS_TESTS" ]; then - WP_VERSION=$WP_VERSION_REAL npm run test - elif [ "1" = "$PHPCS" ]; then - if [ "trunk" = "$TRAVIS_BRANCH" ]; then - composer run-script check-cs-errors - else - composer run-script check-cs-errors -- $( git diff --name-only --diff-filter=ACMR $TRAVIS_COMMIT_RANGE ) - fi - else - composer run-script tests-run - fi - -after_script: - - | - if [ "E2E" = "$LLMS_TRAVIS_TESTS" ]; then - ./vendor/bin/llms-env down - fi - -notifications: - slack: - on_success: change - on_failure: always - rooms: - - secure: VzwXDPjuNCrKed9ACY7dwzyIjcnt6G1iC1LnKAOIx9fyPZ7TARLIf5bSa9M7P5w4uQHK7kpm5yFNtPHKGwaazZnCZxH8jcDMc4M8y3w6j9uNlbidOgfrCpp07lY6kpd8ViR7ANZ4V5Noz+ts8/gSA0yUib6vGP87s6RKHTyVTfNuFmHui7t6vF3S1VCXm4JmOrqmZbY9DlN+8JcyE0Ao3KOk/UDSCZICqo7cYnMci2oHGfb+2VRu49B61tASnV0r/dRu7gjEQTtqwElIJfuP0hGeAYc6bee5vFLA4EIdz2TMgr/Fm1El5eIg+1ZB4bOVEHzUlonLLGaUlqcYfKtmmYiV8BBnte1xBlEflLxYj92ethTUtTvkicVmtK50IlyL8kpb4WBwhXMEjSoKGLmdfaeNGKZ0vS/BnyDA0eWmt4EQ5ZVQL50ukhvmOAXhMB5T+K6Bg6T3yJzXIxej0MrSSNVygpeIwl5RqleXOKJJtJe3TsrsQfdqidXVrKAGSrwlwDRSMLC7JN3l99+5PEXzgb106TE0TBgrMOEClTVyH4gAjplqQ70diw9SAp0rnU518dTDj9HMvZ7KcGQgnAzKI82iB1LaWsWrMjqHtPbn/h+2vRDQNRnx8umnCmC8ezRr4l+xZ8Cb9KgrhvJW+bed3pQFmD/LerSuW6ZgHFsN/KI= diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 95277530da..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,23 +0,0 @@ -# This file is for unifying the coding style for different editors and IDEs -# editorconfig.org - -# WordPress Coding Standards -# https://developer.wordpress.org/coding-standards/wordpress-coding-standards/ - -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -tab_width = 4 -indent_style = tab -insert_final_newline = true -trim_trailing_whitespace = true - -[*.txt] -trim_trailing_whitespace = false - -[*.{md,json,yml,yml.template}] -indent_style = space -indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f64ba5c34e..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * ESlint config - * - * @package LifterLMS/Scripts/Dev - * - * @since Unknown - * @version Unknown - */ - -const config = require( '@lifterlms/scripts/config/.eslintrc.js' ); - -module.exports = config; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 0987bc53de..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,4 +0,0 @@ -* @thomasplevy - -# Full Site Editing. -includes/class-llms-block-templates.php @eri-trabiccolo diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index ff7716d08f..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,67 +0,0 @@ -Contributing to LifterLMS -========================= - -We 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: - -*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/).* - - -### Ways to Contribute - -+ [Submit bug and issues reports](#reporting-a-bug-or-issue) -+ [Contribute new features](#contributing-new-features) -+ [Contribute new code or bug fixes / patches](#contributing-code) -+ [Translate and localize LifterLMS](#contribute-translations) - - -### Reporting a Bug or Issue - -Bugs and issues can be reported at [https://github.com/gocodebox/lifterlms/issues/new/choose](https://github.com/gocodebox/lifterlms/issues/new). - -Before 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. - -Also check our [known issues and conflicts](https://lifterlms.com/doc-category/lifterlms/known-conflicts/) for possible resolutions. - -### Contributing New Features - -When 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. - -First 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). - -If 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. - -LifterLMS 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. - -### Contributing Code - -+ Fork the repository on GitHub. -+ [Install LifterLMS for development](../docs/installing.md). -+ Create a new branch from the 'trunk' branch. -+ Make the changes to your forked repository. -+ 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). -+ Run PHPCS and ensure the output has no errors. We **will** reject pull requests if they fail codesniffing. -+ 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! -+ 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. -+ 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). -+ Push the changes to your fork -+ Submit a pull request to the 'dev' branch of the LifterLMS repo. -+ 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! - - -### Contribute Translations - -All translations to LifterLMS can be made via our GlotPress project at [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/lifterlms). - -Anyone 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/). - -We'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 submit an application at [translate.lifterlms.com](https://translate.lifterlms.com/become-a-translator/). - - -### Need Help Getting Started as a Contributor? - -A number of resources are available for first time contributors: - -+ 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. -+ Check out the [LifterLMS Contributor's Events Calendar](https://make.lifterlms.com/calendar/events/) for opportunities to interact with other contributors. -+ 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. -+ 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) diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md deleted file mode 100644 index d4e16ce6f4..0000000000 --- a/.github/ISSUE_TEMPLATE/Bug_Report.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: Bug Report -about: Report a bug or issue - ---- - -### Reproduction Steps - -+ Include clear and detailed step by step instructions on how the issue can be reliably reproduced -+ Include screenshots where applicable -+ Record a video if possible (if you post a video please still include a text version of your recreation steps!) - - -### Expected Behavior - -+ Include a concise description of what you expected to happen (but didn't) - - -### Actual Behavior - -+ Include a concise description of what actually happens (but isn't supposed to) - - -### Error Messages / Logs - -+ Include any relevant error messages or log files -``` - - -``` - -### System and Environment Information - -
-System Report - - -``` - - -``` - -
- - -This issue has be recreated: -+ [ ] Locally -+ [ ] On a staging site -+ [ ] On a production website -+ [ ] With only LifterLMS and a default theme - -### Browser, Device, and Operating System Information - -+ Browser name and version -+ Operating System name and version -+ Device name and version (if applicable) diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md deleted file mode 100644 index 747c6694f2..0000000000 --- a/.github/ISSUE_TEMPLATE/Feature_Request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature request -about: Suggest an idea or new feature for LifterLMS - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md deleted file mode 100644 index 364675a501..0000000000 --- a/.github/ISSUE_TEMPLATE/Question.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Question -about: Questions or 'how to' about LifterLMS - ---- - -Remember 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: - -- Support Forums: https://wordpress.org/support/plugin/lifterlms -- Official Support Tickets: https://lifterlms.com/my-account/my-tickets -- LifterLMS Community Slack Channel: https://lifterlms.com/slack - -You may also wish to peruse our documentation at https://lifterlms.com/docs - -If none of these places seem appropriate ask away here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index a10d16eb82..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Description - - -Fixes # - -## How has this been tested? - - - - -## Screenshots - -## Types of changes - - - - - -## Checklist: -- [ ] My code has been tested. -- [ ] My code passes all existing automated tests. -- [ ] My code follows the LifterLMS Coding & Documentation Standards. - diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index 3d180165c4..0000000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,20 +0,0 @@ -Security Policy ---------------- - -## Supported Versions - -LifterLMS 3.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. - -| Version | Supported | -| ------- | ------------------ | -| 4.x | :white_check_mark: | -| 3.x | :x: | -| 2.x | :x: | -| 1.x | :x: | - - -## Reporting a Vulnerability - -The 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. - -To report a vulnerability, please see our guidelines at https://lifterlms.com/security/ diff --git a/.github/lifterlms-logo.png b/.github/lifterlms-logo.png deleted file mode 100644 index c5e921a77a..0000000000 Binary files a/.github/lifterlms-logo.png and /dev/null differ diff --git a/.github/sponsors/browserstack-logo.png b/.github/sponsors/browserstack-logo.png deleted file mode 100644 index d420b92984..0000000000 Binary files a/.github/sponsors/browserstack-logo.png and /dev/null differ diff --git a/.github/workflow-matrix.yml b/.github/workflow-matrix.yml deleted file mode 100644 index 21816f162b..0000000000 --- a/.github/workflow-matrix.yml +++ /dev/null @@ -1,11 +0,0 @@ -### -# -# Custom workflow matrix configurations -# -# @link https://github.com/gocodebox/.github/tree/trunk/.github/actions/setup-matrix -# -### -Test PHPUnit: - __delete: - # Remove the LLMS Nightly job (intended for add-ons). - - include[2] diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 719d869e56..0000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: "CodeQL" - -on: - pull_request: - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['javascript'] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml deleted file mode 100644 index f3302244ec..0000000000 --- a/.github/workflows/coding-standards.yml +++ /dev/null @@ -1,60 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/coding-standards.yml} -# -### -name: Coding Standards - -on: - workflow_dispatch: - pull_request: - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - phpcs: - name: Check Coding Standards - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.0' - coverage: none - tools: composer, cs2pr - - # Composer Install. - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install Composer dependencies - run: composer update - - # Check Coding Standards. - - name: Run PHPCS - run: composer run check-cs-errors -- --report-full --report-checkstyle=./phpcs-report.xml - - - name: Show PHPCS results in PR - run: cs2pr ./phpcs-report.xml diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml deleted file mode 100644 index a0c7b29559..0000000000 --- a/.github/workflows/contributors.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Contributors - -on: - workflow_dispatch: - push: - branches: - - trunk - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - - build: - name: Update contributors - runs-on: ubuntu-latest - - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - token: ${{ secrets.ORG_WORKFLOWS }} - - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: '14' - cache: 'npm' - - - name: Install dependencies - run: npm install contributor-faces - - - name: Update README.md - run: ./node_modules/.bin/contributor-faces -e '*\[bot\]' -l 100 - - - name: Commit Updates - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Update contributors list - branch: trunk - file_pattern: README.md - commit_user_name: contributors-workflow[bot] - commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml deleted file mode 100644 index 690cb1fbc8..0000000000 --- a/.github/workflows/issue-triage.yml +++ /dev/null @@ -1,64 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflows/issue-triage.yml} -# -### -name: New Issue Triage and Assignment - -on: - issues: - types: [ opened ] - -jobs: - handle-new-issue: - runs-on: ubuntu-latest - env: - PRIMARY_CODEOWNER: '@thomasplevy' - steps: - - name: Checkout - uses: actions/checkout@v2 - - # Add to project. - ################# - - name: Add to "Triage" project - uses: alex-page/github-project-automation-plus@v0.8.1 - with: - project: Triage - column: Awaiting Triage - repo-token: ${{ secrets.ORG_WORKFLOWS }} - - # Assign to the project's CODEOWNER. - #################################### - - name: Check CODEOWNERS file existence - id: codeowners_file_exists - uses: andstor/file-existence-action@v1 - with: - files: .github/CODEOWNERS - - - name: Parse CODEOWNERS file - id: codeowner - if: steps.codeowners_file_exists.outputs.files_exists == 'true' - uses: SvanBoxel/codeowners-action@v1 - with: - path: .github/CODEOWNERS - - - name: Update PRIMARY_CODEOWNER env var - if: steps.codeowners_file_exists.outputs.files_exists == 'true' - run: | - echo PRIMARY_CODEOWNER=$( echo '${{ steps.codeowner.outputs.codeowners }}' | jq -r '."*"[0]' ) >> $GITHUB_ENV - - - name: Strip @ from username - run: | - echo "PRIMARY_CODEOWNER=${PRIMARY_CODEOWNER#?}" >> $GITHUB_ENV - - - name: Assign issue - uses: pozil/auto-assign-issue@v1 - with: - repo-token: ${{ secrets.ORG_WORKFLOWS }} - assignees: ${{ env.PRIMARY_CODEOWNER }} \ No newline at end of file diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml deleted file mode 100644 index bda391ca3c..0000000000 --- a/.github/workflows/lint-js.yml +++ /dev/null @@ -1,54 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/lint-js.yml} -# -### -name: Lint JavaScript - -on: - workflow_dispatch: - pull_request: - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v1 - with: - node-version: '14' - cache: 'npm' - - - name: Install npm dependencies - run: npm ci - - - name: Run linter - continue-on-error: true - run: npm run lint:js - - - name: Save linter output - continue-on-error: true - run: npm run lint:js -- --output-file eslint-report.json --format json - - - name: Create annotations - uses: ataylorme/eslint-annotate-action@1.2.0 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - report-json: "eslint-report.json" diff --git a/.github/workflows/ossar-analysis.yml b/.github/workflows/ossar-analysis.yml deleted file mode 100644 index 3676b8d673..0000000000 --- a/.github/workflows/ossar-analysis.yml +++ /dev/null @@ -1,44 +0,0 @@ -# This workflow integrates a collection of open source static analysis tools -# with GitHub code scanning. For documentation, or to provide feedback, visit -# https://github.com/github/ossar-action -name: OSSAR - -on: - pull_request: - -jobs: - OSSAR-Scan: - # OSSAR runs on windows-latest. - # ubuntu-latest and macos-latest support coming soon - runs-on: windows-latest - - steps: - # Checkout your code repository to scan - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Install dotnet, used by OSSAR - - name: Install .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.201' - - # Run open source static analysis tools - - name: Run OSSAR - uses: github/ossar-action@v1 - id: ossar - - # Upload results to the Security tab - - name: Upload OSSAR results - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} diff --git a/.github/workflows/packages-test-and-lint.yml b/.github/workflows/packages-test-and-lint.yml deleted file mode 100644 index c22d7007c2..0000000000 --- a/.github/workflows/packages-test-and-lint.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Packages Lint & Test - -on: - workflow_dispatch: - pull_request: - paths: - - 'packages/**' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: '14' - - - name: Cache node_modules - uses: actions/cache@v2 - id: npm-cache - with: - path: node_modules - key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} - - - name: Install NPM Dependencies - if: steps.npm-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Run linter - continue-on-error: true - run: npm run pkg:lint:js - - - name: Save linter output - continue-on-error: true - run: npm run pkg:lint:js -- --output-file eslint-report.json --format json - - - name: Create annotations - uses: ataylorme/eslint-annotate-action@1.2.0 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - report-json: "eslint-report.json" - - test: - name: Test - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: '14' - - - name: Cache node_modules - uses: actions/cache@v2 - id: npm-cache - with: - path: node_modules - key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} - - - name: Install NPM Dependencies - if: steps.npm-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Run test suite - # uses: artiomtr/jest-coverage-report-action@v2.0-rc.6 - uses: gocodebox/jest-coverage-report-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - skip-step: install - threshold: 50 - test-script: npm run pkg:test -- --coverageReporters="text" --coverageReporters="text-summary" diff --git a/.github/workflows/php-test-coverage.yml b/.github/workflows/php-test-coverage.yml deleted file mode 100644 index e45794dc25..0000000000 --- a/.github/workflows/php-test-coverage.yml +++ /dev/null @@ -1,69 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/php-test-coverage.yml} -# -### -name: PHP Code Coverage Report - -on: - workflow_dispatch: - pull_request: - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - - -jobs: - - check-secret: - name: "Check for required secret" - runs-on: ubuntu-latest - outputs: - has-secret: ${{ steps.check-secret.outputs.has-secret }} - steps: - - name: Test secret - id: check-secret - run: | - if [ ! -z "${{ secrets.CC_TEST_REPORTER_ID }}" ]; then - echo "::set-output name=has-secret::true" - fi - - test: - name: "PHP Test Coverage" - runs-on: ubuntu-latest - - needs: check-secret - - if: ${{ 'true' == needs.check-secret.outputs.has-secret }} - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Environment - uses: gocodebox/.github/.github/actions/setup-phpunit@trunk - with: - php-version: "8.0" - wp-version: "5.8" - coverage: "xdebug" - env-file: ".github/.env.php-test-coverage" - secrets: ${{ toJSON( secrets ) }} - - - name: Run Tests with Coverage & Upload Coverage Report - uses: paambaati/codeclimate-action@v2.7.5 - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - RUN_CODE_COVERAGE: "1" - with: - coverageCommand: composer run tests -- --coverage-clover clover.xml diff --git a/.github/workflows/pr-ready.yml b/.github/workflows/pr-ready.yml deleted file mode 100644 index ff275fdfa3..0000000000 --- a/.github/workflows/pr-ready.yml +++ /dev/null @@ -1,26 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflows/pr-ready.yml} -# -### -name: PR Ready for Review - -on: - pull_request_target: - types: [ ready_for_review, review_requested ] - -jobs: - add-to-project: - runs-on: ubuntu-latest - steps: - - uses: alex-page/github-project-automation-plus@v0.8.1 - with: - project: Active - column: Ready for Review - repo-token: ${{ secrets.ORG_WORKFLOWS }} diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml deleted file mode 100644 index 2a7ad7be03..0000000000 --- a/.github/workflows/sync-branches.yml +++ /dev/null @@ -1,35 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/sync-branches.yml} -# -### -name: Sync Branches -on: - push: - branches: - - trunk - workflow_dispatch: - -jobs: - sync: - name: trunk -> dev - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - token: ${{ secrets.ORG_WORKFLOWS }} - fetch-depth: 0 - - name: Perform sync - run: | - git config --global user.name "branch-sync[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout dev - git pull origin trunk --no-ff - git status - git push origin dev diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml deleted file mode 100644 index b4f04be1eb..0000000000 --- a/.github/workflows/test-e2e.yml +++ /dev/null @@ -1,98 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/test-e2e.yml} -# -### -name: Test E2E - -on: - workflow_dispatch: - pull_request: - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - ### - # - # Setup the test matrix. - # - ### - set-matrix: - name: Setup Matrix - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.setup.outputs.matrix }} - steps: - - uses: actions/checkout@v2 - - id: setup - uses: gocodebox/.github/.github/actions/setup-matrix@trunk - - ### - # - # Run tests. - # - ### - test: - name: "WP ${{ matrix.WP }}" - needs: set-matrix - runs-on: ubuntu-latest - continue-on-error: ${{ matrix.allow-failure }} - - strategy: - fail-fast: false - matrix: ${{ fromJSON( needs.set-matrix.outputs.matrix ) }} - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Environment - uses: gocodebox/.github/.github/actions/setup-e2e@trunk - with: - wp-version: ${{ matrix.WP }} - docker-user: ${{ secrets.DOCKER_USERNAME }} - docker-pass: ${{ secrets.DOCKER_PASSWORD }} - node-version: '14' - - - name: Run test suite - run: npm run test -- --verbose - - - name: Upload artifacts - uses: actions/upload-artifact@v2 - if: failure() - with: - name: error-artifacts-wp-${{ matrix.WP }} - path: tmp/artifacts - - ### - # - # Check the status of the entire test matrix. - # - # This will succeed if all jobs from the `test` job's matrix succeed. It allows jobs marked with `allow-failure` - # to fail. - # - # This job can be used as a single status check for branch protection rules. Without this - # we would need to require every job in the above build matrix. - # - ### - status: - name: Test E2E Status - runs-on: ubuntu-latest - if: always() - needs: test - steps: - - name: Check overall matrix status - if: ${{ 'success' != needs.test.result }} - run: exit 1 diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml deleted file mode 100644 index e0988a5f0b..0000000000 --- a/.github/workflows/test-phpunit.yml +++ /dev/null @@ -1,94 +0,0 @@ -### -# -# This workflow file is deployed into this repository via the "Sync Organization Files" workflow -# -# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made -# to the source file. -# -# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml} -# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/test-phpunit.yml} -# -### -name: Test PHPUnit - -on: - workflow_dispatch: - pull_request: - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - - ### - # - # Setup the test matrix. - # - ### - set-matrix: - name: Setup Matrix - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.setup.outputs.matrix }} - steps: - - uses: actions/checkout@v2 - - id: setup - uses: gocodebox/.github/.github/actions/setup-matrix@trunk - - ### - # - # Run tests. - # - ### - test: - name: WP ${{ matrix.WP }} on PHP ${{ matrix.PHP }}${{ matrix.name-append }} - - needs: set-matrix - runs-on: ubuntu-latest - continue-on-error: ${{ matrix.allow-failure }} - - strategy: - fail-fast: false - matrix: ${{ fromJSON( needs.set-matrix.outputs.matrix ) }} - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Environment - uses: gocodebox/.github/.github/actions/setup-phpunit@trunk - with: - php-version: ${{ matrix.PHP }} - wp-version: ${{ matrix.WP }} - llms-branch: ${{ matrix.LLMS }} - env-file: ".github/.env.test-phpunit" - secrets: ${{ toJSON( secrets ) }} - - - name: Run Tests - run: composer run tests - - ### - # - # Check the status of the entire test matrix. - # - # This will succeed if all jobs from the `test` job's matrix succeed. It allows jobs marked with `allow-failure` - # to fail. - # - # This job can be used as a single status check for branch protection rules. Without this - # we would need to require every job in the above build matrix. - # - ### - status: - name: Test PHPUnit Status - runs-on: ubuntu-latest - if: ${{ always() }} - needs: test - steps: - - name: Check overall matrix status - if: ${{ 'success' != needs.test.result }} - run: exit 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8ee30b2117..0000000000 --- a/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Package managers. -node_modules/ -/vendor/ - -# Lock file intentionally excluded to allow easier testing against multiple php versions -# This follows the precedent put forth by the WordPress core {@link https://github.com/WordPress/wordpress-develop/commit/0e442c4615bdcdc1c2e4f8db6f88f57e6859c2ff} -composer.lock - -# Ignore composer-installed libs. -/libraries/* -!/libraries/index.php -!/libraries/README.md - -# Misc. -*.log -.DS_Store - -# Release distribution directory. -/dist/ -/tmp/ - -# Non-distributable configs. -phpunit.xml -.llmsenv diff --git a/.llmsconfig b/.llmsconfig deleted file mode 100644 index 3cf445d10f..0000000000 --- a/.llmsconfig +++ /dev/null @@ -1,71 +0,0 @@ -{ - "build": { - "custom": [ "js-additional", "js-builder" ] - }, - "docs": { - "package": "LifterLMS" - }, - "pot": { - "bugReport": "https://github.com/gocodebox/lifterlms/issues", - "domain": "lifterlms", - "dest": "languages/", - "jsClassname": "LLMS_L10n_JS", - "jsFilename": "class.llms.l10n.js.php", - "jsSince": "3.17.8", - "jsSrc": [ "assets/js/**/*.js", "!assets/js/**/*.min.js", "!assets/js/**/*.js.map" ], - "lastTranslator": "Thomas Patrick Levy ", - "team": "LifterLMS ", - "package": "lifterlms", - "phpSrc": [ - "./*.php", "./**/*.php", - "!vendor/*", "!vendor/**/*.php", "!tmp/**", "!tests/**", "!wordpress/**", - "./vendor/lifterlms/lifterlms-blocks/*.php", "./vendor/lifterlms/lifterlms-blocks/**/*.php", - "./vendor/lifterlms/lifterlms-rest/*.php", "./vendor/lifterlms/lifterlms-rest/**/*.php" - ] - }, - "publish": { - "title": "LifterLMS", - "lifterlms": { - "make": { - "tags": [ 6 ] - }, - "pot": false - } - }, - "scripts": { - "src": [ - "assets/js/**/*.js", - "!assets/js/llms-admin-addons.js", - "!assets/js/**/*.min.js", - "!assets/js/llms-builder*.js", - "!assets/js/app/**/*.js", - "!assets/js/builder/**/*.js", - "!assets/js/partials/**/*.js", - "!assets/js/private/**/*.js" - ], - "dest": "assets/js/" - }, - "watch": { - "custom": [ { - "glob": [ "assets/js/builder/**/*.js", "assets/js/private/**/*.js", "assets/js/app/*.js" ], - "tasks": [ "js-additional", "js-builder" ] - } ] - }, - "zip": { - "composer": true, - "src": { - "custom": [ - "!./**/CHANGELOG.md", - "!./**/README.md", - "!./_private/**", - "!./_readme/**", - "!./docs/**", - "!./packages/**", - "!./wordpress/**", - "!lerna.json", - "!babel.config.js", - "!docker-compose.override.yml.template" - ] - } - } -} diff --git a/.llmsdev.yml b/.llmsdev.yml deleted file mode 100644 index c1076aad17..0000000000 --- a/.llmsdev.yml +++ /dev/null @@ -1,2 +0,0 @@ -pot: - dir: languages diff --git a/.llmsdevrc b/.llmsdevrc deleted file mode 100644 index 5b45e52bda..0000000000 --- a/.llmsdevrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "readme": { - "title": "LMS by LifterLMS - Online Course, Membership & Learning Management System Plugin for WordPress", - "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.", - "meta": { - "Tags": "learning management system, LMS, membership, elearning, online courses, quizzes, sell courses, badges, gamification, learning, Lifter, LifterLMS", - "Requires at least": "5.4", - "Tested up to": "5.8", - "Requires PHP": "7.3" - }, - "changelog": { - "link": "https://make.lifterlms.com/tag/lifterlms/" - }, - "sections": { - "Description": "file:./.wordpress-org/readme/description.md", - "Installation": "file:./.wordpress-org/readme/installation.md", - "Frequently Asked Questions": "file:./.wordpress-org/readme/faqs.md", - "Screenshots": "file:./.wordpress-org/readme/screenshots.md" - } - }, - "i18n": { - "dir": "./languages/" - } -} diff --git a/.llmsenv.dist b/.llmsenv.dist deleted file mode 100644 index 9a229cc9ba..0000000000 --- a/.llmsenv.dist +++ /dev/null @@ -1,2 +0,0 @@ -WORDPRESS_PORT=8080 -WORDPRESS_TITLE=LifterLMS Core e2e diff --git a/.wordpress-org/assets/banner-1544x500.png b/.wordpress-org/assets/banner-1544x500.png deleted file mode 100644 index dba6fb91c6..0000000000 Binary files a/.wordpress-org/assets/banner-1544x500.png and /dev/null differ diff --git a/.wordpress-org/assets/banner-772x250.png b/.wordpress-org/assets/banner-772x250.png deleted file mode 100644 index ea4f22189d..0000000000 Binary files a/.wordpress-org/assets/banner-772x250.png and /dev/null differ diff --git a/.wordpress-org/assets/icon-128x128.png b/.wordpress-org/assets/icon-128x128.png deleted file mode 100644 index 2d2345e018..0000000000 Binary files a/.wordpress-org/assets/icon-128x128.png and /dev/null differ diff --git a/.wordpress-org/assets/icon-256x256.png b/.wordpress-org/assets/icon-256x256.png deleted file mode 100644 index 5f43e48053..0000000000 Binary files a/.wordpress-org/assets/icon-256x256.png and /dev/null differ diff --git a/.wordpress-org/assets/icon.svg b/.wordpress-org/assets/icon.svg deleted file mode 100755 index a6a0a9be5e..0000000000 --- a/.wordpress-org/assets/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/.wordpress-org/assets/screenshot-1.png b/.wordpress-org/assets/screenshot-1.png deleted file mode 100644 index 8506328ad4..0000000000 Binary files a/.wordpress-org/assets/screenshot-1.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-10.png b/.wordpress-org/assets/screenshot-10.png deleted file mode 100644 index 2efc664184..0000000000 Binary files a/.wordpress-org/assets/screenshot-10.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-11.png b/.wordpress-org/assets/screenshot-11.png deleted file mode 100644 index b013a42947..0000000000 Binary files a/.wordpress-org/assets/screenshot-11.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-12.png b/.wordpress-org/assets/screenshot-12.png deleted file mode 100644 index ba68382290..0000000000 Binary files a/.wordpress-org/assets/screenshot-12.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-13.png b/.wordpress-org/assets/screenshot-13.png deleted file mode 100644 index cc1f43ff4f..0000000000 Binary files a/.wordpress-org/assets/screenshot-13.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-14.jpg b/.wordpress-org/assets/screenshot-14.jpg deleted file mode 100644 index abbe7282ea..0000000000 Binary files a/.wordpress-org/assets/screenshot-14.jpg and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-15.png b/.wordpress-org/assets/screenshot-15.png deleted file mode 100644 index e50fe3d91f..0000000000 Binary files a/.wordpress-org/assets/screenshot-15.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-16.png b/.wordpress-org/assets/screenshot-16.png deleted file mode 100644 index 30910ab358..0000000000 Binary files a/.wordpress-org/assets/screenshot-16.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-17.png b/.wordpress-org/assets/screenshot-17.png deleted file mode 100644 index 8d7f03eb42..0000000000 Binary files a/.wordpress-org/assets/screenshot-17.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-18.png b/.wordpress-org/assets/screenshot-18.png deleted file mode 100644 index d1246d607e..0000000000 Binary files a/.wordpress-org/assets/screenshot-18.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-19.png b/.wordpress-org/assets/screenshot-19.png deleted file mode 100644 index 76f403fb60..0000000000 Binary files a/.wordpress-org/assets/screenshot-19.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-2.png b/.wordpress-org/assets/screenshot-2.png deleted file mode 100644 index 1fa8a7011d..0000000000 Binary files a/.wordpress-org/assets/screenshot-2.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-20.png b/.wordpress-org/assets/screenshot-20.png deleted file mode 100644 index 7521b8c172..0000000000 Binary files a/.wordpress-org/assets/screenshot-20.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-21.jpg b/.wordpress-org/assets/screenshot-21.jpg deleted file mode 100644 index 90367b68fb..0000000000 Binary files a/.wordpress-org/assets/screenshot-21.jpg and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-22.png b/.wordpress-org/assets/screenshot-22.png deleted file mode 100644 index 3edb6d4ed2..0000000000 Binary files a/.wordpress-org/assets/screenshot-22.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-23.png b/.wordpress-org/assets/screenshot-23.png deleted file mode 100644 index 53a400098c..0000000000 Binary files a/.wordpress-org/assets/screenshot-23.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-24.png b/.wordpress-org/assets/screenshot-24.png deleted file mode 100644 index 954925ae05..0000000000 Binary files a/.wordpress-org/assets/screenshot-24.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-3.png b/.wordpress-org/assets/screenshot-3.png deleted file mode 100644 index 0270880ff1..0000000000 Binary files a/.wordpress-org/assets/screenshot-3.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-4.png b/.wordpress-org/assets/screenshot-4.png deleted file mode 100644 index bced64a01c..0000000000 Binary files a/.wordpress-org/assets/screenshot-4.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-5.png b/.wordpress-org/assets/screenshot-5.png deleted file mode 100644 index 02b251e4b9..0000000000 Binary files a/.wordpress-org/assets/screenshot-5.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-6.png b/.wordpress-org/assets/screenshot-6.png deleted file mode 100644 index 79fd87e6f0..0000000000 Binary files a/.wordpress-org/assets/screenshot-6.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-7.png b/.wordpress-org/assets/screenshot-7.png deleted file mode 100644 index d1baf75df3..0000000000 Binary files a/.wordpress-org/assets/screenshot-7.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-8.png b/.wordpress-org/assets/screenshot-8.png deleted file mode 100644 index db72b5a00b..0000000000 Binary files a/.wordpress-org/assets/screenshot-8.png and /dev/null differ diff --git a/.wordpress-org/assets/screenshot-9.png b/.wordpress-org/assets/screenshot-9.png deleted file mode 100644 index 1cdceec55f..0000000000 Binary files a/.wordpress-org/assets/screenshot-9.png and /dev/null differ diff --git a/.wordpress-org/readme/01-header.md b/.wordpress-org/readme/01-header.md deleted file mode 100644 index 46135cd280..0000000000 --- a/.wordpress-org/readme/01-header.md +++ /dev/null @@ -1,12 +0,0 @@ -=== LifterLMS - WordPress LMS Plugin === -Contributors: thomasplevy, chrisbadgett, d4z_c0nf, pondermatic, lifterlms, codeboxllc -Donate link: https://lifterlms.com -Tags: learning management system, LMS, membership, elearning, online courses, quizzes, sell courses, badges, gamification, learning, Lifter, LifterLMS -License: GPLv3 -License URI: https://www.gnu.org/licenses/gpl-3.0.html -Requires at least: 5.5 -Tested up to: 5.9 -Requires PHP: 7.3 -Stable tag: {{__VERSION__}} - -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. diff --git a/.wordpress-org/readme/05-description.md b/.wordpress-org/readme/05-description.md deleted file mode 100644 index b65970539b..0000000000 --- a/.wordpress-org/readme/05-description.md +++ /dev/null @@ -1,394 +0,0 @@ -== Description == -[WordPress LMS plugin][home] - **LifterLMS is a powerful WordPress LMS plugin for WordPress that makes it easy to create, sell, and protect engaging online courses and training based membership websites.** LifterLMS is a complete WordPress LMS plugin, course building and LMS solution that works with any well-coded WordPress theme, modern WordPress blocks, and all the popular WordPress page builders (like Elementor, Beaver Builder, Divi, Gutenberg, etc.). As an engaged WordPress community member, LifterLMS actively encourages and helps other great plugins integrate with LifterLMS like Affiliate WP, Monster Insights, WP Fusion, the most popular form plugins, GamiPress, Astra Pro, the Course Scheduler, and many more. You can also connect your WordPress LMS website to 1,500+ other apps via Zapier. LifterLMS is one of only 11 WordPress plugins listed in the Zapier app directory. - -As an innovative self-hosted WordPress LMS platfom solution LifterLMS strikes a beautiful balance in being an **all-in-one WordPress LMS solution** while also integrating with other best of breed technologies relevant to course creators and membership site owners. - -https://www.youtube.com/watch?v=jDVvkipF_pg - -> **Similar to WooCommerce and WordPress**, As a WordPress LMS plugin, LifterLMS gives back to the open source WordPress community by contributing the core LifterLMS plugin for FREE for the world to benefit from. The core LMS incredibly powerful and customizable by itself with it's course building, membership, gamification system, and more. We believe in free distributed learning for all, and our core free open source WordPress LMS plugin helps further tha vision **LifterLMS exists to democratize education in the digital classroom.** - -> **At it's core LifterLMS exists to lift up others through education.** - -You do NOT need a separate ecommerce or membership plugin made by a different company to use LifterLMS! All that and more is included with LifterLMS so you can **avoid the "Software Frankenstein" problem** (too many plugins made by different companies that don't work well together have different levels of support). LifterLMS combines LMS features, course building, membership features, ecommerce feautures, and engagement features into one powerful LMS platform tool. - -LifterLMS is also known for having a thriving well supported LMS user community through active listening, social engagement, a course library and robust documentation. As a feature complete LMS solution, LifterLMS invests heavily in support and it's industry leading customer success program. LifterLMS doesn't just provide LMS software. LifterLMS builds community and invest heavily in supporting the community of LMS site builders. - -LifterLMS uses it's own product to create a helpful course library to help the course building community learn. A company should use it's own software beyond simple demos. Course creation software made by course builders! - -*** - -> We encourage you to get to know the team of online course building experts behind the WordPress LMS plugin by signing up for a **[$1 temporary _30 Day_ website][try]** on our servers with the core LifterLMS plugin AND all the premium LMS add-ons installed. This LMS demo site allows you to test drive the core LMS & all the add-ons before you invest. You can practice creating an online course with LifterLMS's industry leading course builder. Or simply take a course yourself on your demo site to test the course experience out for yourself. You can even add your other favorite plugins & themes to your demo site so you can see them in action together with the LMS. - -> Are you ready to **[Try LifterLMS for $1][try]?** 🚀 - -*** - -You'll see why so many people like you are starting with or switching from another WordPress LMS or hosted platform to [LifterLMS][Home] for online course creation, membership sites, and remote schools. - -# **Who Uses LifterLMS?** - -+ **WordPress Freelancers** -+ **WordPress Agencies** -+ **WordPress Educators** like Shawn Hesketh at [WP101](https://www.wp101.com) -+ IT Departments -+ Marketing Agencies -+ Entrepreneurs -+ CEU Publishers -+ Schools -+ Organizations -+ Governments -+ Enterprise Companies -+ DIY (Do It Yourself course creators, coaches, and entrepreneurs) -+ Instructional Designers -+ WordPress LMS Industry professionals - -https://www.youtube.com/watch?v=RnZflrWG5YQ - -# **What Types of People Use LifterLMS for their WordPress LMS?** - -#### **1) Builders** -The WordPress developers, designers & IT pros who build LMS websites and training portals for clients, employers & themselves - -#### **2) Starters** -Do-it-yourself innovators who are looking to create high value online courses, coaching or training based membership websites with a WordPress LMS - -#### **3) Switchers** -People who have outgrown a hosted LMS platform or an incomplete WordPress LMS stack looking for more power, control and better support - -# **Who Makes The Best WordPress LMS Plugin LifterLMS?** -The LifterLMS team is a **diverse group of talented course creators, developers, designers, marketers and entrepreneurs**. Before developing the LifterLMS product we consulted and built custom WWordPress LMS style training based membership sites for clients all over the world. It was through many years experience building high end custom WordPress LMS websites for the expert industry, that the LifterLMS project was born. - -Because 5 years ago we couldn't find a WordPress LMS plugin that provided a rock solid _all-in-one_ foundation for online course based LMS style training based membership websites, we decided to build LifterLMS and **contribute the core plugin to you and the WordPress community**. - -> LifterLMS is WordPress LMS, course & membership creation software built by course creators and a talented technical team. We understand WordPress, ecommerce, eLearning, course creation, engagement, gamification, conversion optimization, the website building industry, the LMS industry, and the needs of the online teacher coach, and training professional. - -You can learn more about **[the people behind LifterLMS here][team]**. - -# **LifterLMS WordPress LMS By The Numbers ...** - -+ 4,348,041 Course Enrollments powered by LifterLMS -+ 6,570,731 Course and lesson completions powered by LifterLMS -+ 86,807 Achievement badges awarded by LifterLMS -+ 120,728 Certificates awarded by LifterLMS -+ Over 10,000 active installs of the WordPress LMS plugin -+ [181 5 star reviews](https://wordpress.org/support/plugin/lifterlms/reviews/?filter=5) - -# **[LifterLMS Features][features]** - -> _Start with our core free WordPress LMS plugin and [scale-up][price] as your business grows!_ - -#### **Make Money Building an Education-Based Business** -_LifterLMS plus one payment gateway like [Stripe][stripe] or [PayPal][pp] is powerful enough to get you started on your LMS website journey!_ - -+ Credit card payments -+ One-time payments -+ Recurring payments -+ Payment plans -+ Unlimited course and membership pricing models -+ PayPal -+ Subscriptions -+ Checkout -+ Free courses -+ Course bundles -+ Private coaching upsells -+ Course and membership Coupons -+ Bulk course and membership sales -+ Affiliate ready -+ Native course and membership sales pages -+ Offline course and membership sales -+ Customizable course and membership enrollment -+ Country and currency -+ E-commerce dashboard -+ Credit card management -+ Subscription switching -+ Payment switching -+ Native Zapier integration - - -#### **Create Courses on Your WordPress LMS Website** - -+ Course multimedia lessons -+ Course quizzes -+ Course builder -+ Drip Content -+ Course and lesson pre-requisites -+ Course tracks -+ Course assignments -+ Quiz time limits -+ Student dashboard -+ Multi-instructor courses -+ Lesson downloads -+ Course import & export -+ Discussion areas -+ Instructional design -+ Forum integrations -+ Graphics pack -+ Course reviews -+ Group enrollments for courses and memberships - - -#### **Engage Your Students** - -+ Achievement badges -+ Certificates -+ Personalized email -+ Social learning -+ Private coaching -+ Text messaging - -#### **Offer Memberships** - -+ Sitewide membership -+ Course bundles -+ Traditional memberships -+ Automatic course enrollment -+ Bulk course enrollment -+ Content restrictions outside of a course -+ Members-only payment plans -+ Private group discussions -+ Members-only forums - -#### **Integrate your WordPress LMS with the Tools You Need** - -+ Payment gateways -+ Email marketing -+ Forums -+ Mobile friendly -+ Use any theme or page builder -+ Built for compatibility -+ CRMs -+ E-learning authoring tools -+ Tin Can API (xAPI) - -#### **Secure and Protect Your Content** - -+ Course protection -+ User account management and registration -+ Members only content -+ Course only content -+ Restricted access -+ Password management -+ Self-hosted - -#### **Own and Manage Your WordPress LMS Platform** - -+ Detailed course, membership, ecommerce, and student reporting -+ Course gradebook -+ Email notifications -+ Bulk course and membership enrollments -+ Student management -+ Course and membership access management -+ Web design management -+ Branding & typography -+ WordPress LMS User Roles -+ Security -+ Require terms -+ Scalable -+ Layout -+ Testing tools - -#### **Get Support For Your WordPress LMS Project** - -+ Technical support -+ 30 Days of live weekly onboarding calls called [Liftoff Sessions][lift] -+ [Live office hours][oh] -+ [Free training courses][aca] -+ [Free training webinars][webinar] -+ Setup wizard -+ [Detailed documentation][docs] -+ Dynamic resources -+ Demo course -+ System analyzer -+ User community -+ [REST API](https://developer.lifterlms.com/rest-api/) -+ [Developer ecosystem][devblog] -+ [Recommended Resources][resources] for course creators - -#### **Further WordPress LMS Reading** - -+ The [LifterLMS Official Homepage][home] -+ The [LifterLMS Knowledge base][docs] -+ The [LifterLMS Blog][blog] -+ The [LifterLMS Podcast][podcast] -+ The [LifterLMS Academy][aca] -+ The [LifterLMS Developer Blog][devblog] - - -# **Extend and Enhance Your LMS with LifterLMS Add-ons** - -#### **Advanced** - -_Increase your LMS website and it's training program's value with these engagement add-ons_ - -+ [LifterLMS Advanced Quizzes][aq] -+ [LifterLMS Assignments][ass] -+ [LifterLMS Private Areas][pa] -+ [LifterLMS Social Learning][sl] -+ [LifterLMS Advanced Video][av] -+ [LifterLMS Custom Fields][cf] -+ [LifterLMS Groups][gr] -+ [LifterLMS PDFs][pdf] - -#### **Integrations** - -_Integrate your LMS with the third-party tools you know and love_ - -+ [LifterLMS Stripe][stripe] -+ [LifterLMS PayPal][pp] -+ [LifterLMS Authorize.Net][anet] -+ [LifterLMS WooCommerce][wc] -+ [LifterLMS ConvertKit][ck] -+ [LifterLMS MailChimp][mc] - -#### **LMS Website and User Experience Design Tools** - -_Make your online course creations and WordPress LMS platform beautiful_ - -+ [LifterLMS Powerpack][pro] -+ [LifterLMS LaunchPad Theme][lp] - - -#### **Support** - -_**Our world-class LMS software support has your back** and all of our paid products include priority private support with the LifterLMS support team_ - -+ LifterLMS Support Ticket System, ready for any question you have about your LMS -+ Liftoff Sessions access with live screensharing to help you get started with the LMS software -+ [LifterLMS Office Hours][oh] is weekly Mastermind group hosted by LifterLMS CEO Chris Badgett and special guests - -#### **Save Big on your WordPress LMS with a Bundle** - -_Save money while unlocking the full potential of your course building and LMS platform_ - -+ Level up your online course LMS website with our ecommerce, design, marketing technology, and automation tools with the [Universe Bundle][universe] -+ Add even 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][infinity] - - -# **Give The Best WordPress LMS Plugin LifterLMS a Try** - -_There are many ways to take LifterLMS for a test drive before commiting to the WordPress LMS_ - -+ Go ahead and install the free core LifterLMS plugin right now. The free core WordPress LMS plugin is very powerful and customizable. -+ 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. You can also practice creating an online course from scratch and test out the learner experience by enrolling yourself in a course on your demo site. You can even add your other favorite plugins & themes, but this demo site is not something you get to keep after the 30 days are over. **[Try LifterLMS for $1][try]** now. -+ Another way to test LifterLMS out is to see what the student experience is like. Take a **free** course on how to build a LifterLMS website in 20 minutes. [Take a Free Course][demo] now. - -# **Scaling LifterLMS From A Simple Online Course...** - -LifterLMS is incredibly flexible, customizable and scalable. It can be used for a simple one course website, and it can also be used as course marketplace or multi instructor online school. LifterLMS can handle small sites with low course enrollments, and it's also used in large universities and inside fortune 500 corporations for employee training. - -Unlike hosted LMS software where you would pay monthly for access and pay more as your platform grows, LifterLMS does not charge you more per course. LifterLMS also does not charge you more per instructor or per student or based on your revenue. - -Some LifterLMS websites are small in terms of course and membership enrollments by design. Some are quite large in the hundreds of thousand of course enrollments. The largest site we know about has 734,415 course enrollments. - -Wether you are going big or keeping it small, LifterLMS can scale to your needs for your online course, membership site, training portal, or remote school. - -# **What Others Are Saying About LifterLMS for Course Building, Membership Sites, and Remote Schools...** - -> _"I've used a number of course creation and delivery 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, because 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."_ - -> _**Nick Usborne**, Teacher, Entrepreneur_ - -*** - -> _“**WP101.com serves more than 30,000 members**, so it’s no small challenge to migrate to a new membership plugin. **We spent more than a year carefully evaluating dozens of LMS and membership plugins before we finally discovered LifterLMS (a membership plugin and LMS plugin combined into one). It was the only plugin that checked all the boxes for our needs for course creation and membership functionality.** And the LifterLMS team also shares our passion for creating better online learning experiences. In particular, we deeply resonate with their goal of restoring the human touch to online learning—something that is absent from most online courses today.”_ - -> _**Shawn Hesketh**, Owner, WP101_ - -*** - -> _"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!! I’m looking forward to everything that comes next from the creators of LifterLMS!!"_ - -> _**Sarah Lorenzen**, Teacher, Entrepreneur_ - -*** - -> _"LifterLMS has been **the best decision we have made** towards the build out of our course library, online Learning Management System site, and community. The breadth and depth of what LifterLMS offers in a few WordPress plugins exceeds anything else we evaluated as it includes: easy course construction, integrated eCommerce, community capabilities, gamification and the support for delivery of 1-on-1 coaching collaboration services. Lifter also has pre-built integrations with other key WordPress technologies we wanted to use. LifterLMS has attracted a solid community and support network of leading experts to help guide anyone who wants to transform the world or their industry with online training. **Chris and the Lifter team are real people, and they care**."_ - -> _**Michael Wolf**, CEO, emPowering NOW LLC (Golden XPR)_ - -*** - -> _"I bought/installed LifterLMS yesterday then spent the day having a blast! Two years ago I started writing a book, which morphed into wanting to present the material online in a more interactive way. I started my website from scratch in January and installing the WordPress LMS plugin was a milestone moment! A milestone moment that turned out to be one joy right after the other! I'm always amazed when something is made easy! The LifterLMS product is amazing!! Power to the people! **Really quite extraordinary to have something so helpful be able to be in the hands of regular folk**."_ - -> _**Margot Worthy**, Author, Teacher_ - - -# **LifterLMS in Action** -+ [Success Stories][case] — Discover these amazing stories and accomplishments from our community of WordPress LMS website builders. -+ [Showcase][sho] — Check out these WordPress LMS websites using LifterLMS - - -# **Join Our Growing Community of Course Builders, Membership Site Owners, and WordPress LMS Professionals** - -> When you download LifterLMS, you **join a thriving community** of education entrepreneurs, course creators, developers, LMS professionals, and WordPress enthusiasts. We’re one of the fastest growing open source eLearning communities online, and you are welcome here in our LMS community. - -If you’re interested in contributing to LifterLMS, head over to the [LifterLMS GitHub Repository][git] to find out how you can pitch in on the open source WordPress LMS software. - -Want to add a new language to LifterLMS? Swell! You can contribute language translations to the LMS at [translate.wordpress.org][translate]. - -Also I'd like to invite you to the [LifterLMS VIP Facebook group][facebook] so you can check out what other LifterLMS users and course creators are up to and ask questions to the LMS website building community. We also have an engaged [LifterLMS Slack community][slack] with live developer office hours if you would like to connect in Slack. - -**The mission of LifterLMS is to democratize education in the digital classroom. Our vision is to lift up others through education.** - -We invite you to **let us guide you to a successful training platform** through our WordPress LMS technology, course library, and support systems. We want you to avoid the common online course & general LMS website building mistakes, avoid the Software Frankenstein problem, and NOT waste any time bringing your WordPress LMS website to life. - -> LifterLMS helps you **ACCELERATE**. - -# **Here's What I'd Like You To Do Next ...** - -Install the free LifterLMS plugin on your website from here on WordPress, then ... - -**[Try out all the premium WordPress LMS add-ons for $1 by signing up >>HERE<<][try]** - -🚀 - - -[home]: https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[price]: https://lifterlms.com/pricing/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[docs]: https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[blog]: http://blog.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[devblog]: https://make.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[podcast]: http://podcast.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[git]: https://github.com/gocodebox/lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[demo]: https://demo.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[translate]: https://translate.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[facebook]: https://www.facebook.com/groups/lifterlmsvip/ -[slack]: https://join.slack.com/t/lifterlms/shared_invite/enQtMzk3ODczNjc4Mjc3LTBlMmEzMWYyOTIwMDU3NDc2MmRhNGIxNGE0Nzc1OWIxZjg1OGVhM2E5YTkwYzZmMmM1ZTU4MDQxYjVlZDYyZTE -[sho]: https://lifterlms.com/showcase/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[case]: https://lifterlms.com/success/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[lift]: https://blog.lifterlms.com/liftoff/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[aca]: https://academy.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[resources]: https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[team]: https://lifterlms.com/our-team/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[webinar]: https://lifterlms.com/lifterlms-webinars/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale - - -[anet]: https://lifterlms.com/product/authorize-net/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[aq]: https://lifterlms.com/product/advanced-quizzes//?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[ass]: https://lifterlms.com/product/lifterlms-assignments//?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[av]: https://lifterlms.com/product/advanced-video/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale -[dfy]: https://lifterlms.com/dfy/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[cf]: https://lifterlms.com/product/custom-fields/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[ck]: https://lifterlms.com/product/lifterlms-convertkit/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[gr]: https://lifterlms.com/product/groups/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[infinity]: https://lifterlms.com/product/infinity-bundle/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[lp]: https://lifterlms.com/product/launchpad/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[mc]: https://lifterlms.com/product/mailchimp-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[oh]: https://lifterlms.com/product/office-hours/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[pa]: https://lifterlms.com/product/private-areas/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[pdf]: https://lifterlms.com/product/lifterlms-pdfs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[pp]: https://lifterlms.com/product/paypal-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[pro]: https://lifterlms.com/product/lifterlms-pro/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[sl]: https://lifterlms.com/product/social-learning/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[stripe]: https://lifterlms.com/product/stripe-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[try]: https://lifterlms.com/product/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[universe]: https://lifterlms.com/product/universe-bundle/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[wc]: https://lifterlms.com/product/woocommerce-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale - -[features]: https://lifterlms.com/features/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[feature-lms]: https://lifterlms.com/features/lms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[feature-ecomm]: https://lifterlms.com/features/e-commerce/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[feature-membership]: https://lifterlms.com/features/membership/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale -[feature-engagement]: https://lifterlms.com/features/engagement/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale - - diff --git a/.wordpress-org/readme/10-installation.md b/.wordpress-org/readme/10-installation.md deleted file mode 100644 index 13e13fe10c..0000000000 --- a/.wordpress-org/readme/10-installation.md +++ /dev/null @@ -1,34 +0,0 @@ -== Installation == - -#### Minimum System Requirements - -LifterLMS Requires - -+ PHP 7.2 or later -+ MySQL 5.6 or later -+ WordPress 4.0 or later - -Visit 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. - -#### Automatic Installation - -This is the simplest way to install LifterLMS as it utilizes WordPress to handle file transfers and you never need to leave the web browser or admin panel. - -1. Log in to your WordPress dashboard. -2. Navigate to Plugins -> Add New -3. In the search field type "LifterLMS" and click "Search Plugins" -4. Once you've located LifterLMS click "Install Now" -5. Once installation is complete, click "Activate" - -#### Manual Installation - -To manually install LifterLMS you'll need to download the zip file using the "Download" link on this screen. You'll then need to use FTP to manually upload the files to the proper directory on your webserver. - -Please see this [WordPress Codex document](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation) for full instruction on Manual Plugin Installation. - - -#### Setup Wizard - -After installing LifterLMS for the first time you will be redirected to the Setup Wizard. This wizard will walk quickly configure LifterLMS so you can get to course creating as quickly as possible. At the conclusion you'll have the option to import a sample course. - -You 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). diff --git a/.wordpress-org/readme/15-faqs.md b/.wordpress-org/readme/15-faqs.md deleted file mode 100644 index a1ee19e71d..0000000000 --- a/.wordpress-org/readme/15-faqs.md +++ /dev/null @@ -1,62 +0,0 @@ -== Frequently Asked Questions == - -#### Where do I buy LifterLMS add-ons or bundles? - -You can explore the individual 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) - - -#### Are there any troubleshooting steps you'd suggest I try that might resolve my issue before I post a new thread? - -First, make sure that you're running the latest version of LifterLMS. And if you've got any other LifterLMS extensions or themes, make sure those are running the most current version as well. - -The 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. - -Also 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). - - -#### I'm still stuck. Where do I go to file a bug or ask a question? - -Users of the free LifterLMS should post their questions in the plugin's WordPress.org forum. 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. - -If 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. - - -#### LifterLMS is awesome! Can you set it all up for me? - -LifterLMS 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). - - -#### I'm interested in LifterLMS add-ons, but there are a few questions I've got before making the purchase. Can you help me get those addressed? - -Absolutely. If you're not finding your questions answered on the product pages, 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). - - -#### What add-ons are available for LifterLMS, and where can I read more about them? - -You 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) - - -#### I have a feature idea. What's the best way to tell you about it? - -We 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). - - -#### I still have questions. Where can I find answers? - -Be sure you’ve taken the free tutorial training video course: [How to Create an Online Course with LifterLMS](http://demo.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/product/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)**. - - -#### I'm interested in contributing to LifterLMS, how can I start? - -LifterLMS is an open-source project. We manage our team, developers, issues, and code on [GitHub](https://github.com/gocodebox/lifterlms/). - -We 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. - - -#### I found a security vulnerability or issue, how can I report it to the team? - -The 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. - -Please contact team@lifterlms.com to report a security vulnerability. - -You can review our full security policy at [https://lifterlms.com/security-policy](https://lifterlms.com/security-policy). diff --git a/.wordpress-org/readme/20-screenshots.md b/.wordpress-org/readme/20-screenshots.md deleted file mode 100644 index aed6b60253..0000000000 --- a/.wordpress-org/readme/20-screenshots.md +++ /dev/null @@ -1,26 +0,0 @@ -== Screenshots == - -1. LifterLMS Courses -2. LifterLMS Pricing Tables -3. LifterLMS Checkout -4. LifterLMS Lessons -5. LifterLMS Achievement Earned -6. LifterLMS Achievement Badges -7. LifterLMS Quiz Results -8. LifterLMS Student Dashboard -9. LifterLMS Certificates -10. LifterLMS Sales Reporting -11. LifterLMS Student Reporting -12. LifterLMS Enrollment Reporting -13. LifterLMS Sidebar Widgets -14. LifterLMS Subscription Management -15. LifterLMS Settings -16. LifterLMS Course Builder -17. LifterLMS Lesson Settings -18. LifterLMS Engagements -19. LifterLMS Email Engagements -20. LifterLMS Course Access Plans -21. LifterLMS Update Upcoming Order Details -22. LifterLMS Lock Down Non LMS Content with Memberships -23. LifterLMS Membership Course Bundles and Auto Enrollment -24. LifterLMS Business to Business Bulk Enrollment Activations with Vouchers diff --git a/.wordpress-org/readme/25-changelog.md b/.wordpress-org/readme/25-changelog.md deleted file mode 100644 index d38283f06a..0000000000 --- a/.wordpress-org/readme/25-changelog.md +++ /dev/null @@ -1,6 +0,0 @@ -== Changelog == - -{{__CHANGELOG_ENTRIES__}} - - -[Read the full changelog]({{__READ_MORE_LINK__}}) diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 714f3f7318..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,6332 +0,0 @@ -LifterLMS Changelog -=================== - -v5.9.0 - 2022-02-15 -------------------- - -##### Updates and Enhancements - -+ Picture choice questions are now organized using flexbox in favor of a float-powered column layout. -+ Resolved PHP 8.1 deprecation warnings. [#1859](https://github.com/gocodebox/lifterlms/issues/1859) - -##### Bug Fixes - -+ 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) -+ Only allow users with `edit_post` capabilities to bypass content restrictions. -+ Fixed stretched images in quiz description/questions when using the Twenty Twenty-Two theme. [#1976](https://github.com/gocodebox/lifterlms/issues/1976) - -##### Deprecations - -+ Method `LLMS_AJAX::check_voucher_duplicate()` is deprecated in favor of `LLMS_AJAX_HANDLER::check_voucher_duplicate()`. - -##### Updated Templates - -+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/courses/overview.php) -+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/memberships/overview.php) -+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/quizzes/overview.php) -+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/archive-course.html) -+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/archive-llms_membership.html) -+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/single-certificate.html) -+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/single-no-access.html) -+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_cat.html) -+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_difficulty.html) -+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_tag.html) -+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_track.html) -+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-membership_cat.html) -+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-membership_tag.html) -+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/checkout/form-confirm-payment.php) -+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/lesson-navigation.php) -+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/lesson-preview.php) -+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/parent-course.php) -+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/loop-main.php) -+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/loop.php) -+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/myaccount/view-order.php) -+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/quiz/questions/content-picture_choice.php) -+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/quiz/results.php) - - -v5.8.0 - 2022-01-26 -------------------- - -##### New Features - -+ Add theme support for the Twenty Twenty-Two theme. [#1824](https://github.com/gocodebox/lifterlms/issues/1824) -+ Added WordPress Full Site Editing compatibility for various LifterLMS-powered templates. - -##### Updates and Enhancements - -+ The minimum required WordPress core version is now version 5.5. -+ Tested against WordPress version 5.9. -+ 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/). -+ Remove the "description" registered with LifterLMS custom post types. [#710](https://github.com/gocodebox/lifterlms/issues/710) - -##### Updated Templates - -+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/archive-course.html) -+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/archive-llms_membership.html) -+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/single-certificate.html) -+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/single-no-access.html) -+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_cat.html) -+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_difficulty.html) -+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_tag.html) -+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_track.html) -+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-membership_cat.html) -+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-membership_tag.html) -+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/lesson-navigation.php) -+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/lesson-preview.php) -+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/parent-course.php) -+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/loop-main.php) -+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/loop.php) - - -v5.7.0 - 2022-01-11 -------------------- - -##### Updates and Enhancements - -+ Informed developers about the deprecated `LLMS_Section::get_next_available_lesson_order()` method. -+ Informed developers about the deprecated `LLMS_Section::get_order()` method. -+ Informed developers about the deprecated `LLMS_Section::get_parent_course()` method. -+ Informed developers about the deprecated `LLMS_Section::set_parent_course()` method. - -##### Deprecations - -+ Deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` with no replacement. -+ Deprecated the `LLMS_Lesson::get_order()` method in favor of the `LLMS_Lesson::get( 'order' )` method. -+ Deprecated the `LLMS_Lesson::get_parent_course()` method in favor of the `LLMS_Lesson::get( 'parent_course' )` method. -+ Deprecated the `LLMS_Lesson::set_parent_course()` method in favor of the `LLMS_Lesson::set( 'parent_course', $course_id )` method. -+ Deprecated the `LLMS_AJAX_Handler::add_lesson_to_course()` method with no replacement. -+ Deprecated the `LLMS_AJAX_Handler::create_lesson()` method with no replacement. -+ Deprecated the `LLMS_AJAX_Handler::create_section()` method with no replacement. -+ Deprecated the `LLMS_Lesson_Handler::assign_to_course()` method with no replacement. -+ Deprecated the `LLMS_Post_Handler::create_section()` method with no replacement. - -##### Updated Templates - -+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/lesson-navigation.php) -+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/lesson-preview.php) -+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/parent-course.php) - - -v5.6.0 - 2021-12-07 -------------------- - -##### New Features - -+ Added an option to prevent users (by role) from copying site content and saving local copies of images. -+ Added new site setting to disallow concurrent user sessions for specified user roles. - -##### Updates and Enhancements - -+ 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/). - -##### Developer Notes - -+ Database migration functions can now be namespaced, eliminating the need to prefix update function names with a version number. - - -v5.5.0 - 2021-11-05 -------------------- - -##### New Features - -+ Includes the LLMS-CLI beta, a set of WP-CLI commands for LifterLMS and LifterLMS add-ons, as part of the core plugin: - + To get started, run `wp llms --help` in your terminal or read the [online command documentation](https://developer.lifterlms.com/cli/commands/llms/). - + 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. - + 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). - -##### Bug Fixes - -+ Fix AJAX post search when using search queries containing quotes. - -##### Deprecations - -+ The `lifterlms_register_post_type_llms_engagement` is deprecated in favor of `lifterlms_register_post_type_engagement`. -+ The `lifterlms_register_post_type_llms_achievement` is deprecated in favor of `lifterlms_register_post_type_achievement`. -+ The `lifterlms_register_post_type_llms_certificate` is deprecated in favor of `lifterlms_register_post_type_certificate`. -+ The `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`. -+ The `lifterlms_register_post_type_llms_email` is deprecated in favor of `lifterlms_register_post_type_email`. -+ The `lifterlms_register_post_type_llms_coupon` is deprecated in favor of `lifterlms_register_post_type_coupon`. -+ The `lifterlms_register_post_type_llms_voucher` is deprecated in favor of `lifterlms_register_post_type_voucher`. - -##### Developer Notes - -+ The `llms-addons` style asset no longer ships an unminified version. -+ The `llms-admin-add-ons` style asset no longer ships an unminified version and the filename of the distributed file has changed. -+ All the LifterLMS post types are now registered using the static method `LLMS_Post_Types::register_post_type()`. -+ Upgraded woocommerce/action-scheduler to [v3.4.0](https://github.com/woocommerce/action-scheduler/releases/tag/3.4.0). - - -v5.4.1 - 2021-10-26 -------------------- - -##### Bug fixes - -+ 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) -+ Don't sanitize input from user forms until validation has succeeded. [#1829](https://github.com/gocodebox/lifterlms/issues/1829.) -+ 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) - - -v5.4.0 - 2021-10-14 -------------------- - -##### Updates - -+ Added logic to prevent the permanent deletion of courses or memberships with active subscriptions. -+ 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. -+ Updates LifterLMS Blocks to [v2.2.1](https://make.lifterlms.com/2021/09/29/lifterlms-blocks-version-2-2-1/). -+ 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/). - -##### Bug fixes - -+ Fixed issue encountered when cloning lessons with attached assignments. -+ Fixed an error encountered when viewing an order for a deleted course or membership on the student dashboard. - -##### Templates Updated - -+ templates/myaccount/view-order.php - - -v5.3.3 - 2021-10-05 -------------------- - -##### Updates - -+ Update woocommerce/actions-scheduler to version 3.3.0. - -##### Bug fixes - -+ Fixed an issue causing the latest earned achievement to not display on the "My Grades" tab in certain scenarios. -+ Fix issue causing a `waiting...` message to display on the JS dev console. -+ Fix improper usage of `apply_filters_deprecated()` encountered when using deprecated theme settings filters in the course builder. -+ Fixed missing text domain, thanks [chetansatasiya](https://github.com/chetansatasiya)! - -##### Developer notes - -+ Improved the `LLMS.waitFor()` runtime JS dependency loader to output improved debugging information. - - -v5.3.2 - 2021-09-21 -------------------- - -##### Updates - -+ Updated the SendWP integration account management URL. - -##### Bug fixes - -+ Fixed issue encountered with TinyMCE editor instances in repeater metabox groups. -+ Fixed issue causing the latest achievement to not display when reviewing grades on the student dashboard. - - -v5.3.1 - 2021-09-13 -------------------- - -##### Bug fixes - -+ Fixed quote slashing for non-admin roles when editing content in the course builder. -+ The LifterLMS admin icon now uses an encoded SVG to improve admin color scheme compatibility. -+ Fixed an issue with empty admin notices. - -##### Dev updates - -+ The creation date of `llms_orders` is now determined by `llms_current_time()`. - - -v5.3.0 - 2021-08-31 -------------------- - -##### Updates - -+ Improved logic used to determine when a limited length subscription has completed its payment schedule. -+ Improved accessibility of various icon buttons on the admin orders view/edit screen. -+ Improved display of quiz attempts containing questions which have been deleted from the database. -+ POT files from included library plugins (like LifterLMS REST) are now excluded from LifterLMS distributions. - -##### Development updates - -+ Introduced `LLMS_Trait_Singleton` to replace redundant singleton pattern definitions across classes in the codebase. -+ Moveed the loading of the autoloader to the main `lifterlms.php` file. -+ Updated the `LLMS_Payment_Gateway` abstract class to utilize `LLMS_Abstract_Options_Data` for accessing gateway options. -+ Audio and video embed methods shared by `LLMS_Course` and `LLMS_Membership` have been relocated to `LLMS_Trait_Audio_Video_Embed`. -+ Sales page methods shared by `LLMS_Course` and `LLMS_Membership` have been relocated to `LLMS_Trait_Sales_Page`. - -##### Bug Fixes - -+ Fixed a visual issue encountered on the payment confirmation screen on small screens / mobile devices. -+ Fix untranslatable time period strings (day, week, month, and year) found on the admin orders view/edit screen. -+ Fixed an error encountered when attempting to grade a quiz attempt containing deleted questions. - -##### Deprecations - -+ 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. -+ Removed private method `LLMS_Order::calculate_billing_end_date()`. -+ Deprecated the class property `$_instance` from the following classes, use the public method `instance()` instead: - + `LLMS_Achievements` - + `LLMS_Certificates` - + `LLMS_Emails` - + `LLMS_Engagements` - + `LLMS_Events` - + `LLMS_Grades` - + `LLMS_Integrations` - + `LLMS_Notifications` - + `LLMS_Payment_Gateways` - + `LLMS_Processors` - + `LLMS_Sessions` - -##### Templates Updated - -+ templates/checkout/form-confirm-payment.php -+ templates/admin/reporting/tabs/quizzes/attempt.php -+ templates/quiz/results-attempt-questions-list.php - - -v5.2.1 - 2021-08-17 -------------------- - -##### Updates - -+ [LifterLMS Helper Version 3.4.1](https://make.lifterlms.com/2021/08/17/lifterlms-helper-version-3-4-1/). -+ Made minor development-related changes to the `LLMS_Order` class. - -##### Bug Fixes - -+ Fixed an issue encountered when a course or membership sales page redirect is enabled but no URL is saved. - - -v5.2.0 - 2021-08-10 -------------------- - -##### Upcoming Payment Reminder Notification - -+ 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. -+ 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. -+ Props to [@niluzok](https://github.com/niluzok) for doing the initial work required to build this notification! - -##### Updates - -+ 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. -+ Improved internal methods used to generate tables in the body of email notifications. - -##### Bug Fixes - -+ Student registration date is now displayed in the site's timezone in favor of UTC time. -+ Properly pass options `template_path` and `default_path` to the template handler when creating an admin notice using a template. -+ 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. - -##### Deprecations - -+ `LLMS_Install::db_updates()` is deprecated, use ``LLMS_DB_Upgrader::enqueue_updates()` instead. -+ `LLMS_Install::update_notice()` is deprecated with no replacement. -+ Template `admin/notices/db-update.php` is deprecated in favor of `includes/admin/views/db-update.php`. -+ Template `admin/notices/db-updating.php` is deprecated with no replacement. - - -v5.1.3 - 2021-08-04 -------------------- - -+ Bugfix: Fixed an issue where a white box would be output over the certificate background image. -+ Bugfix: Fixed an issue in the course builder causing lessons to be orphaned from a course when moved into an unsaved section. -+ [LifterLMS Helper Version 3.4.0](https://make.lifterlms.com/2021/08/04/lifterlms-helper-version-3-4-0/) - - -v5.1.2 - 2021-07-28 -------------------- - -+ Bugfix: Pass second parameter to the `get_the_excerpt` filter. -+ Fix: Corrected typos in error messages encountered during password reset. - - -v5.1.1 - 2021-07-26 -------------------- - -+ Bugfix: Fixed a bug causing malformed character codes to be rendered in forms when installing forms with translated labels. -+ [LifterLMS Helper version 3.3.1](https://make.lifterlms.com/2021/07/26/lifterlms-helper-version-3-3-1/) - - -v5.1.0 - 2021-07-19 -------------------- - -##### Updates - -+ **Raised the minimum required WordPress core version to 5.8!** -+ Adds WordPress core 5.8 compatibility. -+ Improved user information forms required field validation. -+ Added functionality to ensure that user email and password fields are *always* displayed to logged out users on checkout and registration forms. -+ Added functionality to ensure that user email and password fields are *always* displayed on the account edit form. -+ [LifterLMS Blocks version 2.2.0](https://make.lifterlms.com/2021/07/19/lifterlms-blocks-version-2-2-0/) - -##### Bug fixes - -+ Fixed an issue preventing certain orphaned quizzes from being deleted. -+ Prevent users from submitting a password change without submitting their current password. -+ Allow logged in users to checkout when no form fields are set to display. - - -v5.0.2 - 2021-07-08 -------------------- - -##### LifterLMS Blocks - -+ Upgraded to [version 2.1.1](https://make.lifterlms.com/2021/07/08/lifterlms-blocks-version-2-1-1/). - -##### Bug Fixes - -+ Fixed issue with non-Latin characters in dashboard endpoint URL slugs. -+ Fixed issue preventing address localization when using the [lifterlms_registration] shortcode. - - -v5.0.1 - 2021-06-28 -------------------- - -##### Updates - -+ Update to [LifterLMS Blocks v2.1.0](https://make.lifterlms.com/2021/06/28/lifterlms-blocks-version-2-1-0/). -+ Added a new filter to allow programmatically alter required field validation results. - -##### Bugfixes - -+ Fixed an issue causing preventing form layout options from working when passed into shortcodes. -+ Fixed an issue preventing custom radio, select, and dropdown fields from working during checkout. -+ Fixed an accessibility issue encountered during password strength validation. - - -v5.0.0 - 2021-06-22 -------------------- - -##### User Information Form Builder - -+ Customize all user information collection forms using the block editor for drag and drop and WYSIWYG form building. -+ Customize field labels, placeholders, descriptions and more with an easy point and click interface. -+ Determine if fields are required or optional with a simple toggle switch. -+ Update the form layout with the block editor. Reorder fields, add columns, and more with a simple drag and drop interface. -+ Remove unwanted fields with the click of a button. - -##### User Location Information Form Fields - -+ 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. -+ 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. -+ 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. -+ Both "Country" and "State" fields are now searchable dropdowns elements. -+ The lists of countries and states will be automatically updated during future releases based on information provided by [GeoNames](https://www.geonames.org/) APIs. - -##### Mergecodes everywhere via new `[llms-user]` shortcode - -+ Allows merging most user information field data into any post or page, email, or notification (as well as widgets and more). - -##### Updates - -+ Email and password confirmation fields may now be made optional. -+ "User Information Options" have been largely removed in favor of determining which fields are displayed via the forms UI -+ The former "User Information Options" settings area has been renamed to "User Privacy Options". -+ Removed email lookup logic since `wp_authenticate()` supports email addresses as `user_login` since WP 4.5. -+ Custom user fields added via filters are now displayed on the admin panel at priority 11 instead of 10. -+ Added shortcode processing in LifterLMS-generated emails. -+ If a symbol cannot be found for the supplied currency code, return the code instead of an empty string. - -##### Bug Fixes - -+ Changed the filter on return of `LLMS_Person_Handler::get_password_reset_fields()` from `lifterlms_lost_password_fields` to `llms_password_reset_fields`. -+ Fixed duplicate references to the `llms-select2` script. - -##### Development changes - -+ Added before and after actions hooks for admin tools. -+ 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. -+ 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). -+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state. -+ Stop loading removed processor "table_to_csv". - -##### Library & Vendor Updates - -+ Updates LifterLMS Blocks to version 2.0.1. -+ Updates woocommerce/actions-scheduler to version 3.2.1. -+ Load core libraries from new location and load WP Background Processing lib. -+ The vendor script dependency `topModal.js` has been removed. - -##### Templates Updated - -+ templates/checkout/form-checkout.php -+ templates/checkout/form-confirm-payment.php -+ templates/checkout/form-gateways.php -+ templates/global/form-login.php -+ templates/global/form-registration.php -+ templates/myaccount/form-edit-account.php -+ templates/product/free-enroll-form.php - -##### Deprecations - -The following have been deprecated and will be removed from LifterLMS in a major update following version 5.0.0. - -+ Class Method: `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`. -+ Class Method: `LLMS_Person_Handler::register()` is deprecated, in favor of `llms_register_user()`. -+ Class Method: `LLMS_Person_Handler::sanitize_field()` (private method) is deprecated with no replacement. -+ Class Method: `LLMS_Person_Handler::update()` is deprecated, in favor of `llms_update_user()`. -+ Class Method: `LLMS_Person_Handler::validate_fields()` is deprecated with no replacement. -+ Class Method: `LLMS_Person_Handler::voucher_toggle_script()` is deprecated with no replacement. -+ Filter: `llms_usernames_blacklist` is deprecated, use `llms_usernames_blocklist` instead. -+ Filter: `lifterlms_get_user_custom_fields` is deprecated with no replacement. -+ Function: `llms_get_minimum_password_strength()` is deprecated with no replacement. -+ Option: `lifterlms_registration_generate_username` is deprecated in favor of the new method `LLMS_Forms::are_usernames_enabled()`. - -##### Removed Items - -+ Private method `LLMS_Processors::includes()` has been removed. -+ Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed. -+ Previously deprecated class method `LLMS_Quiz::get_lessons()` has been removed. -+ Previously deprecated class method `LLMS_Controller_Quizzes::take_quiz()` has been removed. -+ Previously deprecated class `LLMS_Processor_Table_To_Csv` has been removed. - - -v5.0.0-rc.2 - 2021-06-18 ------------------------- - -+ Remove password description merge codes from reusable block schema. -+ Explicitly define required field attributes on reusable block schema. -+ Requires WP 5.7 or later to edit forms & show an upgrade nudge when requirements are not met. -+ Add a link from the (now) legacy account settings area to help experienced users find the new form building area -+ Add a (subtle) custom fields add-on upgrade nudge when viewing the forms list on the admin panel -+ Update LifterLMS Blocks to 2.0.0-rc.2 - - -v5.0.0-rc.1 - 2021-06-15 ------------------------- - -+ Updates Action Scheduler library to version 3.2.0 -+ Remove the {min_strength} and {min_length} merge codes from the User Password block description. -+ Don't load removed files during OptimizePress compatibility. -+ Add a 5.0.0 DB upgrade routine and welcome notice -+ Add the LifterLMS Helper as an included library -+ Add WordPress 5.8 compatibility on the Widgets and Customizer screens. -+ Move form location definitions into a schema file -+ Require WordPress 5.7+ to manage forms via the block editor -+ Upgrades LifterLMS Blocks to 2.0.0-rc.1 - - -v5.0.0-beta.2 - 2021-06-01 ---------------------------- - -+ Updates LifterLMS Blocks to 2.0.0-beta.6. -+ (Re-)introduces the user information shortcode as `[llms-user]`. -+ Add Admins status tool to reinstall core forms & reusable blocks. -+ Fixed issue causing data from conditionally disabled fields (like state) from being cleared during form submission -+ Updated form post type labels and added missing labels -+ Removed the previously deprecated class `LLMS_Frontend_Forms` and it's deprecated class methods `reset_password()` and `voucher_check()`. -+ Removed the previously deprecated class `LLMS_Frontend_Password` and it's deprecated class methods: `retrieve_password()`, `check_password()`, and `reset_password()`. -+ Updated country and state localization lists. - - -v5.0.0-beta.1 - 2021-05-19 ---------------------------- - -+ LifterLMS Blocks 2.0.0-beta.5 -+ Added site-wide field name validation -+ Reworked the output of user information fields on the admin panel to share a handler and APIs with frontend fields. -+ Deprecated filter: `lifterlms_get_user_custom_fields` in favor of `llms_admin_profile_fields` -+ Improved previewing of form posts using WP Core block editor UI elements -+ Open Registration form can now always be previewed regardless of the open registration site setting - - -v5.0.0-alpha.6 - 2021-05-07 ---------------------------- - -+ LifterLMS Blocks 2.0.0-beta.4 -+ Fix default reusable password field type from plain text to password -+ Change the default reusable block post titles to reduce confusion when searching for blocks in the editor - - -v5.0.0-alpha.5 - 2021-05-03 ---------------------------- - -+ Reorganized new files into subdirectories. -+ Added serverside password minimum length validation. -+ Fix duplicate password strength meter output. -+ Fix the user password field type from text to password -+ Fix the phone number field type from text to tel -+ Fix user state select field -+ Don't autoload field values from specified datastore when a "value" is explicitly passed to the field. -+ Only load published reusable blocks on the frontend of the website -+ 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. - - -v5.0.0-alpha.4 - 2021-04-26 ---------------------------- - -+ Default form templates now use reusable blocks. -+ Improved the user experience surrounding fields with a confirmation field (email address and password). -+ Added the ability to define a field's column width instead of requiring the usage of WP column blocks. -+ Added support for reusable blocks on form posts -+ Upgraded LifterLMS Blocks to 2.0.0-beta.3. - - -v5.0.0-alpha.3 - 2021-03-23 ---------------------------- - -+ Fixed issue preventing users from editing their email address and password on the dashboard account edit screens. -+ Fixed issues with country names with the article "the" in their name, for example "The Netherlands" instead of "Netherlands The". -+ Upgraded LifterLMS Blocks to version 2.0.0-beta.2. - - -v5.0.0-alpha.2 - 2021-03-22 ---------------------------- - -##### Updates - -+ Updates LifterLMS Blocks to version 2.0.0-beta.1 -+ Adds functionality to force usage of the Block Editor for editing LifterLMS forms -+ Updates localization functionality and methods to have more accurate information. -+ Added a function for determining if open registration is enabled. -+ Added a WP Admin Bar link below the "Edit Page" link to enable editing the form (if a form exists on the page). - -##### Bug Fixes - -+ Fixed an issue encountered when custom HTML fields exist on a form (backwards compatibility for pre 5.x fields API). - - -v5.0.0-alpha.1 - 2021-01-07 ---------------------------- - -##### User Information Form Builder - -+ Customize all user information collection forms using the block editor for drag and drop and WYSIWYG form building. -+ Customize field labels, placeholders, descriptions and more with an easy point and click interface. -+ Determine if fields are required or optional with a simple toggle switch. -+ Update the form layout with the block editor. Reorder fields, add columns, and more with a simple drag and drop interface. -+ Remove unwanted fields with the click of a button. - -##### User Location Information Form Fields - -+ 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. -+ 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. -+ 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. -+ Both "Country" and "State" fields are now searchable dropdowns elements. -+ The lists of countries and states will be automatically updated during future releases based on information provided by [GeoNames](https://www.geonames.org/) APIs. - -##### Mergecodes everywhere via new `[user]` shortcode - -+ TODO. - -##### Updates - -+ Email and password confirmation fields may now be made optional. -+ "User Information Options" have been largely removed in favor of determining which fields are displayed via the forms UI -+ The former "User Information Options" settings area has been renamed to "User Privacy Options". - -##### Bug Fixes - -+ Changed the filter on return of `LLMS_Person_Handler::get_password_reset_fields()` from `lifterlms_lost_password_fields` to `llms_password_reset_fields`. - -##### Development changes - -+ 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. -+ 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). -+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state. - -##### Library & Vendor Updates - -+ Load core libraries from new location and load WP Background Processing lib. -+ The vendor script dependency `topModal.js` has been removed. - -##### Templates Updated - -+ templates/global/form-login.php -+ templates/global/form-registration.php -+ templates/product/free-enroll-form.php - -##### Deprecations - -The following have been deprecated and will be removed from LifterLMS in a major update following version 5.0.0. - -+ Class Method: `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`. -+ Class Method: `LLMS_Person_Handler::register()` is deprecated, in favor of `llms_register_user()`. -+ Class Method: `LLMS_Person_Handler::sanitize_field()` (private method) is deprecated with no replacement. -+ Class Method: `LLMS_Person_Handler::update()` is deprecated, in favor of `llms_update_user()`. -+ Class Method: `LLMS_Person_Handler::validate_fields()` is deprecated with no replacement. -+ Class Method: `LLMS_Person_Handler::voucher_toggle_script()` is deprecated with no replacement. -+ Filter: `llms_usernames_blacklist` is deprecated, use `llms_usernames_blocklist` instead. -+ Function: `llms_get_minimum_password_strength()` is deprecated with no replacement. -+ Option: `lifterlms_registration_generate_username` is deprecated in favor of the new method `LLMS_Forms::are_usernames_enabled()`. - -##### Removed Items - -+ Private method `LLMS_Processors::includes()` has been removed. -+ Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed. -+ Previously deprecated class method `LLMS_Quiz::get_lessons()` has been removed. -+ Previously deprecated class method `LLMS_Controller_Quizzes::take_quiz()` has been removed. -+ Previously deprecated class `LLMS_Processor_Table_To_Csv` has been removed. - - -v4.21.3 - 2021-05-31 --------------------- - -##### Updates - -+ Increase 3rd party support for WP core hook `lostpassword_post` hook. - -##### Bug fixes - -+ 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. -+ Fixed an issue encountered when updating LifterLMS premium add-ons via the LifterLMS Helper encountered when API errors are occur. -+ Updated the failure error code from 'activation' to 'deactivation' in the `LLMS_Add_On` class. -+ Updated the API connection error message returned when using the `LLMS_Abstract_API_Handler` class. - -##### Deprecations - -+ Class `LLMS_Frontend_Password` is deprecated, see deprecated methods and their replacments below: - - + `LLMS_Frontend_Password::retrieve_password()` is deprecated in favor of `LLMS_Controller_Account::lost_password()`. - + `LLMS_Frontend_Password::check_password_reset_key()` is deprecated in favor of `check_password_reset_key()`. - + `LLMS_Frontend_Password::reset_password()` is deprecated in favor of `reset_password()`. - - -v4.21.2 - 2021-05-17 --------------------- - -##### Security Update - -This releases fixes a security issue affecting LifterLMS versions 4.21.1 and earlier: - -+ Thank you to [Amirmohammad vakili](https://www.linkedin.com/in/amirmuhammad-vakili-65a7a11b3/) for reporting an insecure direct object reference issue. - -##### Updates - -+ 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. - -##### Bug fixes - -+ Fixed an issue causing PHP errors when attempting to access a quiz attempt that doesn't exist. -+ Fixed a localization issue encountered when entering transaction amounts on the admin panel. - - -v4.21.1 - 2021-04-29 --------------------- - -##### Security Update - -This releases fixes two security issues affecting LifterLMS versions 4.21.0 and earlier: - -+ Thank you to [Amirmohammad vakili](https://www.linkedin.com/in/amirmuhammad-vakili-65a7a11b3/) for reporting a way to store XSS. -+ Thank you to Ashish Jha from [Bluefire Redteam](https://www.bluefire-redteam.com/) for reporting a reflected XSS issue on checkout screens. - - -v4.21.0 - 2021-04-19 --------------------- - -##### Updates - -+ Certificate exports will now automatically include (most) externally hosted images and stylesheets. -+ Opt-in forward compatibility changes have been made to the `LLMS_Abstract_Options_Data` class. - -##### Bugfixes - -+ Fixed an issue causing one-time payment orders from being included in totals on some reporting screens. -+ Fixed an issue causing student enrollment counts to be incorrect under some circumstances. -+ Fixed issues resulting in unnecessary duplicated instances of course background data processing. -+ Fixed an error encountered when a course is deleted prior to its background data being processed. -+ Fixed an escaping issue causing passwords with a backslash character from being usable following a password reset. - - -v4.20.0 - 2021-03-16 --------------------- - -##### Bugfixes - -+ Fixed an issue causing a fatal error when attempting to access reports for deleted students. Thanks Thanks [@pondermatic](https://github.com/pondermatic)! -+ Fixed an issue encountered on the builder causing the last section to be returned when retrieving the previous section for the first section. - - -v4.19.0 - 2021-03-11 --------------------- - -##### Supported Version Requirement Updates - -+ **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).** -+ **The minimum supported WordPress core version has been raised to version 5.3.** - -##### Bug fixes - -+ Fixed an issue causing TinyMCE editor instances to be unusable within metaboxes when using the block editor. - - -v4.18.0 - 2021-03-04 --------------------- - -**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!** - -##### Updates - -+ Tested up to WordPress core version 5.7 -+ Updated several occurrences of `json_encode()` with preferred `wp_json_encode()`. - -##### Bug fixes - -+ Added a tie-breaker when there are multiple enrollment statuses with the same date & time. Thanks [@pondermatic](https://github.com/pondermatic)! -+ On admin order pages and tables don't print links for deleted students. -+ Fixed an issue on admin order pages when viewing an order for a deleted student. - - -v4.17.0 - 2021-02-22 --------------------- - -##### Updates - -+ The post type feature "llms-sales-page" has been added to course and membership post types, signifying they support custom sales pages. - -##### Bug fixes - -+ Fixed compatibility issues with Yoast SEO 15.8. -+ Fixed duplicate action hook in `content-no-access-after.php` template. -+ Added early returns to several templates to prevent undefined variables errors. -+ Fixed an undefined variable encountered in course builder JS debug logging. - -##### Templates Updated - -+ content-no-access-after.php -+ quiz/meta-information.php -+ quiz/results.php -+ quiz/start-button.php - - -v4.16.0 - 2021-02-18 --------------------- - -##### Updates - -+ Added preview management to the student dashboard to allow previewing of the dashboard as a site visitor. -+ Added a new filter to allow customization of courses output by the [lifterlms_courses] shortcode. Thanks [@reedhewitt](https://github.com/reedhewitt)! -+ Added compatibility code to reduce plugin conflicts encountered in the course builder. Resolves a conflict encountered when building quizzes with Yoast SEO installed. - -##### Bug fixes - -+ Fixed undefined variable error encountered when creating custom notification types. Thanks [@pondermatic](https://github.com/pondermatic)! -+ Fixed incorrect variables passed to `sprintf()` in logging functions used by the course data background processor. Thanks [@pondermatic](https://github.com/pondermatic)! - - -v4.15.0 - 2021-02-09 --------------------- - -##### Updates - -+ Database migration: remove any "orphaned" access plans which were not properly cleaned up during deletion of parent course or membership. -+ Improved performance of membership post association query methods. - -##### Bug fixes - -+ Access plans will now be automatically deleted when their parent course or membership is deleted. -+ Fix an issue with donut charts/graphs on RTL sites. -+ Fix an issue causing unpublished (draft/private) courses from being returned during queries for membership post associations. - -##### LifterLMS REST 1.0.0-beta.15 - -###### Updates - -+ Added Access Plan resource and endpoint. -+ Provide a more significant error message when trying to delete an item without permissions. -+ Use `WP_Http` constants in favor of integers when referencing HTTP status codes. - -###### Bug fixes - -+ Fixes localization issues where a singular name was used in favor of the expected plural form. -+ Fixed issues where an error object was not properly returned when expected -+ Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`. -+ Fixed access plans resource link. -+ Fixed wrong trigger retrieved when multiple trigger were present for the same user/post pair on Student Enrollment resources. - - -v4.14.0 - 2021-02-04 --------------------- - -##### Updates - -+ 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). -+ 5-star review request displayed at 30 enrollments instead of 50. - -##### Bug fixes - -+ Fixed an issue encountered when using shortcodes in the description of an access plan. -+ Fixed an issue encountered when editing auto-draft courses on the course builder. - -##### Deprecations - -+ `LLMS_Controller_Quizzes::take_quiz()` is deprecated in favor of `LLMS_AJAX_Handler::quiz_start()`. -+ Method `LLMS_Quiz::get_lessons()` is deprecated with no replacement. - - -v4.13.0 - 2021-01-26 --------------------- - -##### Updates - -+ **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/). -+ When cloning courses and lessons the cloned post will be created as a draft. -+ When cloning courses the suffix "(Clone)" will be appended to the title of the course to unify cloning behavior with lessons. -+ Added information about LifterLMS specific constant values to the LifterLMS system report. -+ Added a new constant `LLMS_IS_SITE_CLONE` which can be used to force the site's clone status. - -##### Bug fixes - -+ 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. -+ 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. -+ Fixed an issue encountered when unexpected or malformed data is stored in the LifterLMS admin notices option. - - -v4.12.0 - 2021-01-20 --------------------- - -##### Updates - -+ Automatic site clone detection checks have been adjusted to always run in favor of only running on the admin panel. -+ LifterLMS Site Features (like recurring payment status) can now be configured via constant values. -+ Added `llms_load_admin_tools` action to allow 3rd parties to easily hook into our admin tools system. -+ Made numerous performance improvements on the course data background processor. -+ 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. - -##### Bug fixes - -+ Fixed an incorrect HTML `for` attribute and added an `id` to the related input element on the student dashboard voucher redemption endpoint. -+ Fixed a pagination error encountered when using course or membership list shortcodes on the static front page. -+ Make sure `is_lifterlms()` exists before calling it in navigation menu-related classes. - -##### Deprecations - -+ `LLMS_Admin_Notices_Core::check_staging()` is deprecated in favor of `LLMS_Staging::notice()`. -+ Unused property `LLMS_Course::$sections` is replaced by `LLMS_Course::get_sections()`. -+ Unused property `LLMS_Course::$sku` is deprecated with no replacement. -+ `LLMS_Frontend_Forms` is deprecated, functionality is available via `LLMS_Controller_Account`. -+ `LLMS_Frontend_Forms::reset_password()` is deprecated in favor of `LLMS_Controller_Account::reset_password()`. - -##### Templates Updated - -+ templates/myaccount/form-redeem-voucher.php - - -v4.11.0 - 2021-01-07 --------------------- - -##### Updates - -+ Adds the ability to use the Instructors blocks on the membership post type. Thanks [@alaa-alshamy](https://github.com/alaa-alshamy)! -+ Updated LifterLMS Blocks to [Version 1.11.1](https://make.lifterlms.com/2020/12/29/lifterlms-blocks-version-1-11-1/). - -##### Bug fixes - -+ Fixed a PHP Notice encountered when trying to retrieve next lesson from an empty section. - -##### Templates updated - -+ templates/course/author.php - - -v4.10.2 - 2021-01-04 --------------------- - -##### Updates - -+ Improveed performance of `llms_get_enrolled_students()`. -+ Refactored lesson navigation query functions. - -##### Bug fixes - -+ Fixed sorting error when sorting student reports by name. - - -v4.10.1 - 2020-12-10 --------------------- - -##### Bug fixes - -+ Fixed visual issues encountered on the admin Add-Ons screen. -+ Use `hr.wp-header-end` in favor of a second (hidden)

to "catch" admin notices on the Add-Ons screen. -+ Replace incorrect usage of invalid ID `llms_shop` with `courses` during catalog template loader checks. -+ Function `llms_get_post()` will now only allow instantiation of LifterLMS classes. -+ Remove unneeded require autoloaded file `includes/class.llms.quiz.data.php`. - - -v4.10.0 - 2020-12-01 --------------------- - -##### Updates - -+ Adds native theme support for the WordPress default theme Twenty Twenty-One. -+ Improved the `llms_archive_description()` function and releated filter. - -##### Bug fixes - -+ 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)! -+ Fixed an issue encountered when using non-latin characters in a course post URL slug. Thanks [@alaa-alshamy](https://github.com/alaa-alshamy)! - -##### Templates Updated - -+ templates/loop/pagination.php - - -v4.9.0 - 2020-11-24 -------------------- - -+ Tested up to WordPress core 5.6 (RC.1). -+ Raised the minimum required WordPress core version to 5.1. -+ Add new localization utilities for developers. -+ Fixed various issues found on PHP 8. -+ Added script localization for block editor scripts. -+ 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/). -+ Updated LifterLMS Blocks to [Version 1.10.0](https://make.lifterlms.com/2020/11/24/lifterlms-blocks-version-1-10-0/). - - -v4.8.0 - 2020-11-16 -------------------- - -##### Updates - -+ Added additional course imports and templates at the end of the setup wizard -+ Added a cloud importer enabling 1-click importing of courses and course templates via the importer at LifterLMS -> Import -+ Added strict comparisons in several places. -+ Course "extra" data is only added to course arrays during exports to improve performance on the course builder. -+ Improved template override loading performance on sites with no child theme. - -##### Bug fixes - -+ Fixed issues related to reliance on methods provided by the `mb_string` PHP module. - -##### Deprecations - -+ `LLMS_Admin_Setup_Wizard::generator_course_status()` is deprecated with no replacement. -+ `LLMS_Admin_Setup_Wizard::watch_course_generation()` is deprecated with no replacement. - - -v4.7.1 - 2020-11-05 -------------------- - -##### Bug fixes - -+ During import generation set the post excerpt during the initial post insert instead of during metadata updates after creation. - -##### LifterLMS REST API 1.0.0-beta.16 - -+ Improved performance of various database queries. - - -v4.7.0 - 2020-11-02 -------------------- - -##### Updates - -+ Major refractor of the `LLMS_Generator` class. -+ Course export structure improved to include images and reusable blocks found in post content. -+ When importing courses images will be automatically sideloaded into the media library as new attachment posts -+ When importing courses reusable blocks will be imported -+ Improved the success message displayed following a course import -+ The class `LLMS_Admin_Reporting` is now always loaded on the admin panel. -+ Performance improvements have been made to the `LLMS_Events_Query` to support using the `no_found_rows` query argument. -+ 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. -+ Made improvements to the admin payment rescheduler tool to have more accurate reporting information. - -##### Bug fixes - -+ Replaced an instance of the LifterLMS (old) 1.0 rocket logo with the current rocket logo. Thanks [@imknight](https://github.com/imknight)! -+ Ensure builder `switch-number` fields are set with the `number` type attribute. Thanks [@imknight](https://github.com/imknight)! -+ Don't display a "View Post" link when updating post types that aren't publicly queryable. Thanks [@imknight](https://github.com/imknight)! -+ Fixed the incorrect output of an achievment's title in a popover notification when using the {{ACHIEVEMENT_TITLE}} merge code. Thanks [@CadenG150](https://github.com/@CadenG150)! -+ Fixed an error encountered when plugins utilize the `WP_Users_List_Table` class outside of the `users.php` screen. - -##### Deprecations - -+ `LLMS_Admin_Import::localize_stat()` is deprecated with no replacement. -+ `LLMS_Admin_Users_Table::load_dependencies()` is deprecated with no replacement. The included class, `LLMS_Admin_Reporting` is now always loaded. -+ `LLMS_Generator::add_custom_values()` is deprecated in favor of `LLMS_Generator_Courses::add_custom_values`. -+ `LLMS_Generator::get_author_id_from_raw()` is deprecated in favor of `LLMS_Generator_Courses::get_author_id_from_raw()`. -+ `LLMS_Generator::get_default_post_status()` is deprecated in favor of `LLMS_Generator_Courses::get_default_post_status()`. -+ `LLMS_Generator::get_generated_posts()` is deprecated in favor of `LLMS_Generator::get_generated_content()`. -+ `LLMS_Generator::format_date()` is deprecated in favor of `LLMS_Generator_Courses::format_date()`. -+ `LLMS_Generator::increment()` is deprecated with no replacement. - - -v4.6.0 - 2020-10-19 -------------------- - -+ Added an admin tool to help automatically identify and schedule missed recurring payments -+ Use `llms_deprecated_function()` in favor of `llms_log()`. -+ Removed logging and use `apply_filters_deprecated()` in favor of `apply_filters()`. - - -v4.5.1 - 2020-10-14 -------------------- - -##### Updates - -+ Added logic in `LLMS_Database_Query` to reduce unnecessary DB reads when total results are not required. - -##### Bug fixes - -+ Removed the course "Excerpt" area in favor of utilization of the course sales page content. -+ Show sales reporting currency symbol based on LifterLMS site options in favor of the browser's locale settings. -+ Fixed an issue causing achievement-related JS DOM events to be bound unnecessarily. Thanks to [@imknight](https://github.com/imknight)! -+ Fixed an issue causing site administrator capabilities to be removed during LifterLMS data removal. -+ 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)! -+ Only display the admin bar "View Manager" to users who can bypass content restrictions. -+ Updated jQuery code to stop using deprecated events and methods in preparation for jQuery upgrades in the WordPress core. -+ Fixed PHP notice encountered on the admin panel when using Yoast SEO. - - -v4.5.0 - 2020-10-06 -------------------- - -##### Updates - -+ 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! -+ When accessing a certificate that does not have sharing enabled, a 404 will be served in favor of an error message. -+ Admin payment gateway notices will no longer redisplay a week after being dismissed. -+ Log files will be automatically split when a file is 5MB or larger, ensuring that log files never grow too large. -+ During student registration, `wp_signon()` is used to login the newly created user. -+ Improved slow background process database queries run during the automatic "closing" of idle user sessions. - -##### Bug fixes - -+ `LLMS_User_Certificate::get_related_post_id()` and `LLMS_User_Certificate::get_user_id()` will now always return an integer. -+ Fixes issues related to account sign on/out and session start/end events being recorded incorrectly. - -##### Deprecations - -+ `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()`. - - -v4.4.4 - 2020-09-21 -------------------- - -##### Bug fixes - -+ Don't pass unsupported parameter `$use_cache` to the `calculate_grade()` method, thanks [@pondermatic](https://github.com/pondermatic)! -+ Add an HTML title attribute to the admin setup wizard page. -+ Fix issue causing notices to be logged during quiz attempt deletion on the admin panel. - -##### Deprecations - -+ Method `LLMS_Admin_Setup_Wizard::scripts()` & `LLMS_Admin_Setup_Wizard::output_step_html()` are deprecated with no replacements. - -##### LifterLMS REST API version 1.0.0-beta.15 - -+ Bugfix: Created lessons will now have the derivative `course_id` property set according to the ID of the lesson's parent section. -+ Bugfix: The `course_id` property of lessons is now properly marked as read-only. - - -v4.4.3 - 2020-09-16 -------------------- - -+ Bugfix: Fix engagement email duplicate check issue. -+ Bugfix: Fix transposition issue found in engagement email dupcheck debug log message. - - -v4.4.2 - 2020-09-08 -------------------- - -+ Bugfix: Fix lesson navigation regression introduced in 4.4.0. - - -v4.4.1 - 2020-09-04 -------------------- - -+ 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. -+ Bugfix: Fixed regression introduced in 4.4.0 preventing the `certificates.css` stylesheet from loading on certificate screens. -+ Update: Engagement email related logs will be logged to a separate logfile, `engagement-emails` in favor of the main `llms` log. - - -v4.4.0 - 2020-09-02 -------------------- - -##### Updates - -+ Improved LifterLMS static asset registration, queuing, definitions, and management. -+ Added strict comparators in various areas of the codebase. - -##### Changes to deprecated function logs and warnings - -+ The `llms_deprecated_function()` method now uses `_deprecated_function()` (from the WP core) under the hood. -+ LifterLMS deprecation warnings are logged to the WP core `debug.log` file in favor of the LifterLMS log file. -+ LifterLMS deprecation warnings will now trigger a `E_USER_DEPRECATED` error when `WP_DEBUG` is enabled. - -##### Bugfixes - -+ Fixed a lesson navigation issue encountered when sections contain unpublished lessons. -+ Fixed an undefined variable notice encountered on the student dashboard. -+ Fixed an issue encountered when the `wp_login_url()` function returns an empty string. -+ Fixed a double slash found in an asset URI. - -##### Deprecations - -+ `LLMS_Frontend_Assets::is_inline_script_enqueued()` is deprecated in favor of `LLMS_Frontend_Assets::is_inline_enqueued()`. -+ `LLMS_Ajax::register_script()` is deprecated with no replacement. -+ `LLMS_Ajax::get_ajax_data()` is deprecated with no replacement. -+ Javascript AJAX nonce variable is moved from `wp_ajax_data.nonce` to `window.llms.ajax-nonce`. - -##### Templates Updated - -+ templates/checkout/form-gateways.php -+ templates/course/lesson-preview.php -+ templates/course/syllabus.php - - -v4.3.3 - 2020-08-17 -------------------- - -+ Fixed an issue causing legends of reporting charts to be truncated and only readable after a mouse hover. -+ Fixed an issue caused by passing `null` values to `wp_insert_post()`. -+ Fixed a javascript error encountered on LifterLMS settings screens. - - -v4.3.2 - 2020-08-10 -------------------- - -+ WP 5.5 compatibility: Automatically deregister "protected" post types from wp-sitemap.xml. - - -v4.3.1 - 2020-08-06 -------------------- - -+ When resetting tracking data cookies, set a "secure" cookie where possible. -+ Catch an unhandled error encountered when generating certificate exports. -+ When an error is encountered during certificate export generation, display an error notice instead of a general notice. - - -v4.3.0 - 2020-07-28 -------------------- - -##### Security Fix - -+ Fixed an XSS issue on account edit and registration forms. Thanks to [Morningstar](https://twitter.com/0xMstar) for reporting this issue! - -##### Bug fixes - -+ Fixed an error encountered during customizer live theme preview encountered when Twenty-twenty is the current theme. -+ 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. -+ Set the `$type` property of the `LLMS_Event` class to `event`. -+ Set the `$type` property of the `LLMS_Quiz_Attempt` class to `quiz_attempt`. -+ Set the `$type` property of the `LLMS_User_Post_Meta` class to `user_postmeta`. - -##### Updates - -+ Added a filter `llms_form_field_args` to allow extending form fields prior to HTML rendering. - -##### Deprecations - -The 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. - -+ `llms__created` has been deprecated, use `llms_{$type}_created` where `{$type}` is the database record type defined by the class property. -+ `llms__deleted` has been deprecated, use `llms_{$type}_deleted` where `{$type}` is the database record type defined by the class property. -+ `llms__updated` has been deprecated, use `llms_{$type}_updated` where `{$type}` is the database record type defined by the class property. - - -v4.2.0 - 2020-07-21 -------------------- - -##### Updates - -+ Admins can now preview the checkout screen as visitors or students using the "View As" function from the WP Admin bar -+ Javascript cookies now set cookies with `sameSite` set to `strict` as recommended by Firefox/Mozilla. -+ Added filters to allow 3rd parties to use LifterLMS completion tracking APIs to "complete" external or non-LMS content. -+ Added "deep" orphan checks when checking the relationship between a quiz and a lesson. -+ Normalized the return structure in `LLMS_Post_Instructors::get_instructors()` when no instructor set, thanks [@nicolas-jaussaud](https://github.com/nicolas-jaussaud)! -+ Update LifterLMS rocket icon used in the WP Admin Bar in the "View As" area. - -##### Bug fixes - -+ When deleting a quiz attempt the related lesson will now be automatically marked as "Incomplete" when appropriate. -+ `LLMS_Abstract_User_Data::get_id()` now always returns an integer. -+ Fixed a 404 error resulting from settings tooltips referencing a missing icon asset. -+ Added logic to set the order status to 'cancelled' when an enrollment linked to an order is deleted. - - - -v4.1.0 - 2020-07-06 -------------------- - -##### LifterLMS REST 1.0.0-beta.14 - -+ **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. -+ 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. - - -v4.0.0 - 2020-06-25 -------------------- - -This 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/). - -##### Bug Fixes - -+ Fixed an issue encountered during quiz grading. -+ Add RTL language support for popover interfaces found throughout the course builder. -+ Fixed issue encountered in MySQL 8.0 when using the bbPress integration. - -##### LifterLMS REST API 1.0.0-beta.13 - -+ Bugfix: Fixed error response messages on the instructors endpoint. -+ Bugfix: Fixed student progress deletion endpoint issues preventing progress from being fully removed. - -##### Action Scheduler Library - -Switches 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). - -While this is a semantically major upgrade of the library there are no backwards incompatible changes to the public API. - -There 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. - -##### Deprecations - -+ Function `LLMS()` is deprecated in favor of `llms()`. - -##### Templates Modified - -+ templates/global/form-login.php -+ templates/global/form-registration.php - -##### Miscellaneous Breaking Changes - -**WP Session Manager Library** - -Removes the bundled WP Session Manager plugin dependency, all public methods included with this plugin have been removed without direct replacements. - -**Removed JS dependencies** - -Removes bundled JS bootstrap 3 dependencies: "collapse" and "transition" - -**Removed CSS Classes** - -Removes classnames from student dashboard login and registration form wrapper elements which conflict with bootstrap causing visual issues. - -These 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... - -+ templates/global/form-login.php: Removes `col-1` class from the `div.llms-person-login-form-wrapper` element. -+ templates/global/form-registration.php: : Removes `col-2` class from the `div.llms-new-person-form-wrapper` element. - -**Removed SVG assets and functionality** - -+ LifterLMS no longer utilizes SVGs powered by the `LLMS_Svg` class. The class has been deprecated and removed (see below). -+ The `assets/svg` directory (and all SVG assets contained within) has been removed. -+ The constant `LLMS_SVG_DIR` has been removed. - -##### Previously deprecated classes (and files) that have been removed - -+ `LLMS_Admin_Analytics`: `includes/admin/class.llms.admin.analytics.php` -+ `LLMS_Analytics`: `includes/class.llms.analytics.php` -+ `LLMS_Analytics_Courses`: `includes/admin/analytics/class.llms.analytics.courses.php` -+ `LLMS_Analytics_Memberships`: `includes/admin/analytics/class.llms.analytics.memberships.php` -+ `LLMS_Analytics_Page`: `includes/admin/analytics/class.llms.analytics.page.php` -+ `LLMS_Analytics_Sales`: `includes/admin/analytics/class.llms.analytics.sales.php` -+ `LLMS_Course_Basic`: `includes/class.llms.course.basic.php` -+ `LLMS_Course_Handler`: `includes/class.llms.course.handler.php` -+ `LLMS_Course_Factory`: `includes/class.llms.course.factory.php` -+ `LLMS_Lesson_Basic`: `includes/class.llms.lesson.basic.php` -+ `LLMS_Meta_Box_Expiration`: `includes/admin/post-types/meta-boxes/class.llms.meta.box.expiration.php` -+ `LLMS_Meta_Box_Video`: `includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php` -+ `LLMS_Number`: `includes/class.llms.number.php` -+ `LLMS_Person`: `includes/class.llms.person.php` -+ `LLMS_Quiz_Legacy`: `includes/class.llms.quiz.legacy.php` -+ `LLMS_Svg`: `includes/class.llms.svg.php` -+ `LLMS_Table_Questions`: `includes/admin/reporting/tables/llms.table.questions.php` -+ `LLMS\Users\User`: `includes/Users/User.php` - -##### Previously deprecated class properties that have been removed - -+ `LifterLMS->person` (generally accessed via `LLMS()->person`). -+ `LLMS_Analytics_Widget->date_end` -+ `LLMS_Analytics_Widget->date_start` -+ `LLMS_Analytics_Widget->output` -+ `LLMS_Certificate->enabled` -+ `LLMS_Course_Data->$course` -+ `LLMS_Course_Data->$course_id` - -##### Previously deprecated class methods that have been removed: - -+ `LLMS_Admin_Table::queue_export()` -+ `LLMS_AJAX::get_achievements()` -+ `LLMS_AJAX::get_all_posts()` -+ `LLMS_AJAX::get_associated_lessons()` -+ `LLMS_AJAX::get_certificates()` -+ `LLMS_AJAX::get_courses()` -+ `LLMS_AJAX::get_course_tracks()` -+ `LLMS_AJAX::get_emails()` -+ `LLMS_AJAX::get_enrolled_students()` -+ `LLMS_AJAX::get_enrolled_students_ids()` -+ `LLMS_AJAX::get_lesson()` -+ `LLMS_AJAX::get_lessons()` -+ `LLMS_AJAX::get_lessons_alt()` -+ `LLMS_AJAX::get_memberships()` -+ `LLMS_AJAX::get_question()` -+ `LLMS_AJAX::get_sections()` -+ `LLMS_AJAX::get_sections_alt()` -+ `LLMS_AJAX::get_students()` -+ `LLMS_AJAX::update_syllabus()` -+ `LLMS_Course::get_children_sections()` -+ `LLMS_Course::get_children_lessons()` -+ `LLMS_Course::get_author()` -+ `LLMS_Course::get_author_id()` -+ `LLMS_Course::get_author_name()` -+ `LLMS_Course::get_sku()` -+ `LLMS_Course::get_id()` -+ `LLMS_Course::get_title()` -+ `LLMS_Course::get_permalink()` -+ `LLMS_Course::get_user_postmeta_data()` -+ `LLMS_Course::get_user_postmetas_by_key()` -+ `LLMS_Course::get_checkout_url()` -+ `LLMS_Course::get_start_date()` -+ `LLMS_Course::get_end_date()` -+ `LLMS_Course::get_next_uncompleted_lesson()` -+ `LLMS_Course::get_lesson_ids()` -+ `LLMS_Course::get_syllabus_sections()` -+ `LLMS_Course::get_short_description()` -+ `LLMS_Course::get_syllabus()` -+ `LLMS_Course::get_user_enroll_date()` -+ `LLMS_Course::get_user_post_data()` -+ `LLMS_Course::check_enrollment()` -+ `LLMS_Course::is_user_enrolled()` -+ `LLMS_Course::get_student_progress()` -+ `LLMS_Course::get_membership_link()` -+ `LLMS_Lesson::get_assigned_quiz()` -+ `LLMS_Lesson::get_drip_days()` -+ `LLMS_Lesson::mark_complete()` -+ `LLMS_PlayNice::divi_fb_wc_product_tabs_after()` -+ `LLMS_PlayNice::divi_fb_wc_product_tabs_before()` -+ `LLMS_PlayNice::wc_is_account_page()` -+ `LLMS_Post_Instructors::get_defaults()` -+ `LLMS_Query::set_dashboard_pagination()` -+ `LLMS_Query::add_query_vars()` -+ `LLMS_Question::get_correct_option()` -+ `LLMS_Question::get_correct_option_key()` -+ `LLMS_Question::get_options()` -+ `LLMS_Quiz::get_assoc_lesson()` -+ `LLMS_Quiz::get_passing_percent()` -+ `LLMS_Quiz::get_remaining_attempts_by_user()` -+ `LLMS_Quiz::get_time_limit()` -+ `LLMS_Quiz::get_total_allowed_attempts()` -+ `LLMS_Quiz::get_total_attempts_by_user()` -+ `LLMS_Quiz_Attempt::get_status()` -+ `LLMS_Shortcode_My_Account::lost_password()` -+ `LLMS_Section::count_children_lessons()` -+ `LLMS_Section::delete()` -+ `LLMS_Section::get_children_lessons()` -+ `LLMS_Section::remove_all_child_lessons()` -+ `LLMS_Section::remove_child_lesson()` -+ `LLMS_Section::set_order()` -+ `LLMS_Section::set_title()` -+ `LLMS_Section::update()` -+ `LLMS_Session::init()` -+ `LLMS_Session::maybe_start_session()` -+ `LLMS_Session::set_expiration_variant_time()` -+ `LLMS_Session::set_expiration_time()` -+ `LLMS_Session::use_php_sessions()` -+ `LLMS_Student::delete_quiz_attempt()` -+ `LLMS_Student::get_best_quiz_attempt()` -+ `LLMS_Student::get_quiz_data()` -+ `LLMS_Student::has_access()` -+ `LLMS_Student_Dashboard::output_courses_content()` -+ `LLMS_Student_Dashboard::output_dashboard_content()` -+ `LLMS_Student_Dashboard::output_notifications_content()` -+ `LLMS_Widget_Course_Progress::widget_contents()` - -##### Previously deprecated functions that have been removed - -+ `is_filtered()` -+ `lifterlms_template_loop_view_link()` -+ `llms_add_user_table_columns()` -+ `llms_add_user_table_rows()` -+ `llms_create_new_person()` -+ `llms_get_question()` -+ `llms_get_quiz()` -+ `llms_set_user_password_rest_key()` -+ `llms_setup_product_data()` -+ `llms_setup_question_data()` -+ `llms_verify_password_reset_key()` - -##### Previously deprecated hooks that have been removed - -+ Action: `lifterlms_before_memberships_loop_item_title` -+ Action: `lifterlms_after_memberships_loop_item_title` -+ Action: `lifterlms_after_memberships_loop_item_title` -+ Filter: `lifterlms_completed_transaction_message` -+ Filter: `lifterlms_is_filtered` -+ Filter: `lifterlms_get_analytics_pages` -+ Filter: `lifterlms_analytics_tabs_array` - -##### Previously deprecated shortcodes that have been removed - -+ `[courses]` -+ `[lifterlms_user_statistics]` - -##### Previously deprecated templates that have been removed - -+ `templates/loop/view-link.php` - -##### Previously deprecated global variables that have been removed - -+ `$product` -+ `$question` - - -v3.41.1 - 2020-06-23 --------------------- - -+ Apply restrictions to post content and excerpts during WP REST requests. - - -v4.0.0-rc.1 - 2020-06-18 ------------------------- - -View 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/). - - -v3.41.0 - 2020-06-12 --------------------- - -##### Bug Fixes - -+ Fix issues encountered when a user role with the `edit_users` capability has multiple LifterLMS roles (like Student). - -##### LifterLMS 4.0.0 Release Preparation - -LifterLMS 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. - -The following is a list of items that have not been previously deprecated but will be removed from LifterLMS 4.0.0. - -For 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/). - -##### Deprecations - -The 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. - -The bundled Javascript Boostrap 3 modules, "collapse" and "transition" are deprecated from our codebase and are being removed. - -The following CSS classes are deprecated and will be removed: - -+ `templates/global/form-login.php`: The `col-1` class from the `div.llms-person-login-form-wrapper` element will be removed. -+ `templates/global/form-registration.php`: : The `col-2` class from the `div.llms-new-person-form-wrapper` element will be removed. - -The following classes are deprecated: - -+ `LLMS_Number`: `includes/class.llms.number.php` -+ `LLMS_Person`: `includes/class.llms.person.php` -+ `LLMS_Table_Questions`: `includes/admin/reporting/tables/llms.table.questions.php` - -The following class methods are deprecated: - -+ `LLMS_PlayNice::divi_fb_wc_product_tabs_after()` -+ `LLMS_PlayNice::divi_fb_wc_product_tabs_before()` -+ `LLMS_Question::get_correct_option()` -+ `LLMS_Question::get_correct_option_key()` -+ `LLMS_Quiz::get_passing_percent()`, use `LLMS_Quiz::get( 'passing_percent' )` instead. -+ `LLMS_Quiz::get_assoc_lesson()`, use `LLMS_Quiz::get( 'lesson_id' )` instead. -+ `LLMS_Session::init()` -+ `LLMS_Session::maybe_start_session()` -+ `LLMS_Session::set_expiration_variant_time()` -+ `LLMS_Session::set_expiration_time()` -+ `LLMS_Session::use_php_sessions()` - -The following class properties are deprecated: - -+ `LifterLMS->person` (generally accessed via `LLMS()->person`). - -The following functions are deprecated: - -+ `lifterlms_template_loop_view_link()` -+ `llms_add_user_table_columns()` -+ `llms_add_user_table_rows()` -+ `llms_get_question()` -+ `llms_get_quiz()` -+ `llms_setup_product_data()` -+ `llms_setup_question_data()` - -The following global variables are deprecated: - -+ `$product` -+ `$question` - -The following action hooks are deprecated: - -+ `lifterlms_before_memberships_loop_item_title` -+ `lifterlms_after_memberships_loop_item_title` -+ `lifterlms_after_memberships_loop_item_title` - -The following template file is deprecated: - -+ `templates/loop/view-link.php` - - -v4.0.0-beta.3 - 2020-06-10 --------------------------- - -View 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/). - - -v3.40.0 - 2020-06-09 --------------------- - -##### Updates - -+ Adds a 1-click installation connector for the MailHawk email delivery plugin. - -##### Bugfixes - -+ Fixed an issue encountered during checkout when using a coupon against an access plan with a free trial. - -##### Deprecations - -+ `LLMS_SendWP::do_remote_install()` will be converted to a protected method and should no longer be called directly. -+ `LLMS_Abstract_Email_Provider::output_css()` - -##### Templates updated - -+ templates/checkout/form-gateways.php - - -v4.0.0-beta.2 - 2020-06-04 --------------------------- - -View 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/). - - -v4.0.0-beta.1 - 2020-06-01 --------------------------- - -View 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/). - - -v3.39.0 - 2020-05-28 --------------------- - -+ Student Welcome notifications and user registered engagements now fire when users are created via the REST POST requests to the `/students` endpoint. -+ Bugfix: Error encountered when printing full-page certificates on certain themes. - -##### LifterLMS REST 1.0.0-beta.12 - -+ Feature: Added the ability to filter student and instructor collection list requests by various user information fields. -+ Fix: Prevent infinite loops encountered when invalid API keys are utilized. -+ Fix: Add an action used to fire LifterLMS core engagement and notification emails - - -v3.38.2 - 2020-05-19 --------------------- - -+ Added a default question type ("choice") to prevent malformed questions from being inadvertently stored in the database. -+ When retrieving question data from the database, automatically fall back to the default question type value if no question type is saved. - - -v3.38.1 - 2020-05-11 --------------------- - -+ Update: Added methods for retrieving a list of posts associated with a membership. -+ Bug fix: Fixed an issue causing certificate backgrounds to be cropped or cut in certain circumstances. -+ Bug fix: Fixed an issue generating certificate downloads on servers where `mime_content_type()` does not exist. -+ Bug fix: Fixed an issue which caused bbPress course forum restrictions to stop working. - - -v3.38.0 - 2020-04-29 --------------------- - -##### Updates - -+ 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. -+ 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). -+ 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. -+ Updates LifterLMS Blocks to version 1.9.0 - -##### Bug fixes - -+ Fixed an issue causing free access plans to bypass course enrollment restrictions like capacity and enrollment time periods. -+ 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. -+ Fixed an issue causing deprecation theme-compatibility related deprecation notices to be incorrectly thrown. -+ 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. -+ Updated the way notification background processors are dispatched. This fixes an issue in the LifterLMS Twilio add-on. - -##### Deprecations - -+ `LLMS_Notifications::dispatch_processors()` is deprecated in favor of async dispatching via `LLMS_Notifications::schedule_processors_dispatch()`. - -##### Templates Updated - -+ templates/product/pricing-table.php - -##### LifterLMS Blocks - -+ Update: Improved script dependencies definitions. -+ Update: Updated asset paths for consistency with other LifterLMS projects. -+ Update: Updated various WP Core references that have been deprecated (maintains backwards compatibility). -+ Update: The Lesson Progression block is no longer rendered server-side in the block editor (minor performance improvement). -+ Update: Converted the course progress block into a dynamic block. Fixes an issue allowing the progress block to be visible to non-enrolled students. -+ Update: Added a filter on the output of the Pricing Table block: `llms_blocks_render_pricing_table_block`. -+ Bug fix: Fixed an issue encountered when using the WP Core "Table" block. -+ Bug fix: Fixed a few areas where `class` was being used instead of `className` to define CSS classes on elements in the block editor. -+ Bug fix: Fixed a user-experience issues encountered on the Course Information block when all possible information is disabled. -+ Bug fix: Fixed an issue causing visibility attributes to render on blocks that don't support them. -+ Bug fix: Fixed an issue preventing 3rd party blocks from modifying default block visibility settings. -+ Bug fix: Fixed a spelling error visible inside the block editor. -+ Bug fix: Fixed an issue causing the "Course Progress" block to be shown to non-enrolled students and visitors. -+ Bug fix: Removed redundant CSS from frontend. -+ Bug fix: Stop outputting editor CSS on the frontend. -+ 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. -+ Changes to the Classic Editor Block: - + 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. - + 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. - - -v3.37.19 - 2020-04-20 ---------------------- - -##### Updates - -+ Added a new debugging tool to clear pending batches created by background processors. -+ 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. - -##### Bug Fixes - -+ Added localization to strings on the coupon admin screen. Thanks [parfilov](https://github.com/parfilov)! -+ Fixed issue encountered in metaboxes when the `$post` global variable is not set. - - -v3.37.18 - 2020-04-14 ---------------------- - -+ Fix regression introduced in version 3.34.0 which prevented checkout success redirection to external domains. -+ Resolved a conflict with LifterLMS, Divi, and WooCommerce encountered when using the Divi frontend pagebuilder on courses and memberships. -+ Fixed issue causing localization issues when creating access plans, thanks [@mcguffin](https://github.com/mcguffin)! - - -v3.37.17 - 2020-04-10 ---------------------- - -##### Updates - -+ Updated the lost password and password reset form handlers for improved error handling and extendability by other plugins. - -##### Bug Fixes - -+ Fixed a conflict with WooCommerce resulting in password reset issues on the WooCommerce account dashboard. -+ Fixed an issue allowing voucher codes from deleted vouchers to still be redeemed. -+ Fixed an issue with pagination on the courses tab of a users BuddyPress profile. -+ Fixed a typo in the `post_status` query arg when retrieving access plans for a course or membership. - -##### Deprecations - -+ `LLMS_PlayNice::wc_is_account_page()` is no longer required and is deprecated with no replacement -+ WP core `get_password_reset_key()` should be used in favor of `llms_set_user_password_rest_key()`. -+ WP core `check_password_reset_key()` should be used in favor of `llms_verify_password_reset_key()`. - - -v3.37.16 - 2020-03-31 ---------------------- - -+ Bugfix: Fix issue causing student dashboard notification view to work incorrectly. - - -v3.37.15 - 2020-03-27 ---------------------- - -##### Security Notice - -**This releases fixes a security issue. Please upgrade immediately!** - -Props to [Omri Herscovici and Sagi Tzadik from Check Point Research](https://www.checkpoint.com/) who found and disclosed the vulnerability resolved in this release. - -##### Updates & Bug Fixes - -+ Excluded `page.*` events in order to keep the events table small. -+ Fixed error encountered when errors encountered validating custom fields. Thanks to [@wenchen](https://github.com/wenchen)! -+ Fixed issue causing course pagination issues in certain scenarios. - -##### LifterLMS REST API Version 1.0.0-beta.11 - -+ Bugfix: Correctly store user `billing_postcode` meta data. -+ Bugfix: Fixed issue preventing course.created (and other post.created) webhooks from firing. - - -v3.37.14 - 2020-03-25 ---------------------- - -+ Update: Added the ability to view the PHP error log file (as defined by `ini_get( 'error_log' )` ) on the LifterLMS -> Status -> Logs page. -+ Update: Added strict comparisons for various condition checks. -+ Bugfix: Fixed an issue where users might be redirected to the wrong course following a course import at the conclusion of the setup wizard. -+ Bugfix: Fixed issue with tracking event data being lost due to cookie size limitations. -+ Bugfix: Fixed issue potentially encountered when checking user capabilities for certificates and achievements. -+ Bugfix: Fixed an issue preventing additional instances of the JS `LLMS.Storage` class from being instantiated. - - -v3.37.13 - 2020-03-10 ---------------------- - -+ Remove usage of internal functions marked as deprecated. - - -v3.37.12 - 2020-03-10 ---------------------- - -##### Updates - -+ Tested up to WordPress Core version 5.4. -+ Added support for post revisions for course, lesson, and mebership post types. - -##### Developer updates - -+ Added strict comparisons for various condition checks. -+ 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. - -##### Bugfixes - -+ Fixed the name of the "actions" column on the quiz reporting screen. -+ Fixed PHP warnings resulting from functions used to exclude order notes from comment counts. -+ Fixed issue causing order notes to be included in the count displayed on the admin comments list despite their exclusion from the table itself. -+ Fixed PHP notice thrown on the WordPress menu editor interface encountered when student dashboard endpoints have been deleted or removed. -+ Fixed issue causing quotes to be encoded in various email, achievement, and certificate fields. - -##### Deprecations - -The following have been deprecated with no replacements and will be removed in the next major update: - -+ `LLMS_Course_Factory::get_course()` -+ `LLMS_Course_Factory::get_lesson()` -+ `LLMS_Course_Factory::get_product()` -+ `LLMS_Course_Factory::get_quiz()` -+ `LLMS_Course_Factory::get_question()` -+ `LLMS_Course_Handler::get_users_not_enrolled()` - - -v3.37.11 - 2020-03-03 ---------------------- - -##### Updates - -+ Resolved a conflict with the "Starter Templates" plugin which made it impossible to edit quizzes while the plugin was enabled. - -##### Bugfixes - -+ Fixed an issue causing lesson post authors to be "lost" when adding an existing lesson to a course. -+ Fixed an issue causing php notices to be generated during existing lesson addition on the course builder. -+ Fixed an issue causing course bbPress forums to be lost when editing that course using the "Quick Edit" function from the courses table. - -##### LifterLMS REST v1.0.0-beta.10 - -+ Added text domain to i18n functions that were missing the domain. -+ Added a "trigger" parameter to enrollment-related endpoints. -+ Added `llms_rest_enrollments_item_schema`, `llms_rest_prepare_enrollment_object_response`, `llms_rest_enrollment_links` filter hooks. -+ Fixed setting roles instead of appending them when updating user, thanks [@pondermatic](https://github.com/pondermatic)! -+ Fixed return when the enrollment to be deleted doesn't exist, returns `204` instead of `404`. -+ Fixed 'context' query parameter schema, thanks [@pondermatic](https://github.com/pondermatic)! - - -v3.37.10 - 2020-02-19 ---------------------- - -+ Update: Exclude the privacy policy page from the sitewide restriction. -+ Update: Added filter `llms_enable_open_registration`. -+ Fix: Notices are printed on pages configured as a membership restriction redirect page. -+ Fix: Do not apply membership restrictions on the page set as membership's restriction redirect page. -+ Fix: Added flag to print notices when landing on the redirected page. - - -v3.37.9 - 2020-02-11 --------------------- - -+ Updated CSS classes used in privacy policy text suggestions per changes in WordPress core 5.3. Thanks [@garretthyder](https://github.com/garretthyder)! -+ Added privacy exported group descriptions. Thanks [@garretthyder](https://github.com/garretthyder)! -+ 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. -+ Added option for admin settings fields to show an asterisk for required fields. -+ Added option for integration plugins can now add automatically generated "Settings" link to the plugins screen. -+ Bugfix: Fixed an IE compatibility issue related to usage of `Object.assign()`. - - -v3.37.8 - 2020-01-21 --------------------- - -+ Fix: Student quiz attempts are now automatically deleted when a quiz is deleted. -+ Fix: "Orphaned" quizzes (those with no parent course and/or lesson) can be deleted from the Quiz reporting table. -+ 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. - - -v3.38.0-beta.2 - 2019-12-19 ---------------------------- - -+ Update LifterLMS Blocks to v1.7.3. - - -v3.38.0-beta.1 - 2019-12-13 ---------------------------- - -##### Form Management Improvments - -+ Forms (registration, checkout, account) are now managed via a block editor interface. -+ Customize field labels, description, and placeholders in a simple WYSIWYG interface. -+ Mark fields as required with a toggle. -+ Reorder fields with drag and drop. -+ Customize layout using block editor columns. -+ Use LifterLMS block-level visibility to conditionally display fields based on enrollment or logged in status. - -##### Form Localization - -+ Added default country and state/region lists (see the "languages" directory). -+ Country and state forms are now searchable dropdowns that adjusted based on the currently selected country. -+ 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. -+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state. - -##### Updates - -+ New shortcode `[user]` which is used to output user information in a merge code interface. -+ Improved form field generation via `LLMS_Form_Field` class. -+ LifterLMS Settings: renamed "User Information Options" to "User Privacy Options". -+ Reorganized open registration setting. -+ Use `LLMS.wait_for()` for dependency waiting. -+ Moved checkout template variable declarations to the checkout shortcode controller. -+ Removed field display settings in favor of form customization using the form editors. -+ Organized function files. Some functions have been moved. -+ Function `llms_get_minimum_password_strength_name()` now accepts a parameter to retrieve strength name by key. -+ Use `LLMS.wait_for()` for dependency waiting. - -##### LifterLMS Blocks v1.6.0 - -+ Feature: Added form field blocks for use on the Forms manager. -+ Feature: Add logic for `logged_in` and `logged_out` block visibility options. -+ Update: Added isDisabled property to Search component. -+ Update: Adjusted priority of `render_block` filter to 20. -+ Bug fix: Import `InspectorControls` from `wp.blockEditor` in favor of deprecated `wp.editor` -+ Bug fix: Automatically store course/membership instructor with `post_author` data when the post is created. -+ Bug fix: Pass style rules as camelCase. - -##### Removed unused Javascript assets - -+ Remove unused bootstrap transiton and collapse scripts. -+ Remove topModal vendor dependency. -+ Remove password strength inline enqueues. - -##### Bug fixes - -+ Only attempt to add a nonce to the datastore when a nonce exists in the settings object. - -##### Deprecations - -+ Deprecated `LLMS_Person_Handler::register()` method, use `llms_register_user()` instead. -+ Deprecated `llms_get_minimum_password_strength()` with no replacement. - -##### Template Updates - -+ templates/checkout/form-checkout.php -+ templates/checkout/form-gateways.php -+ templates/global/form-registration.php - -v3.37.7 - 2020-01-08 --------------------- - -+ Fix error resulting from undefined default value. -+ Fix PHP 7.4 deprecation notice. - - -v3.37.6 - 2019-12-12 --------------------- - -+ New transaction creation date is now specified using `llms_current_time()`. -+ Use the last successful transaction time to calculate from when the previously stored next payment date is in the future. -+ Fixed an issue causing transaction post titles to be recorded with missing data due to invalid `strftime()` placeholders. - - -v3.37.5 - 2019-12-09 --------------------- - -+ 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. - - -v3.37.4 - 2019-12-06 --------------------- - -##### Bug Fixes - -+ Fixed a bug causing certificate _template_ exports to export the site's homepage instead of the certificate preview. -+ When exporting a certificate template, use the `post_author` to determine what user to use for merge code data. -+ Revert Accounts settings tab page id to "account". - -##### LifterLMS Blocks v1.7.1 - -+ Feature: Add logic for `logged_in` and `logged_out` block visibility options. -+ Update: Added `isDisabled` property to Search component. -+ Update: Adjusted priority of `render_block` filter to 20. -+ Update: Added filter, `llms_block_supports_visibility` to allow modification of the return of the check. -+ Update: Disabled block visibility on registration & account forms to prevent a potentially confusing form creation experience. -+ Update: Added block editor rendering for password type fields. -+ Update: Perform post migrations on `current_screen` instead of `admin_enqueue_scripts`. -+ Update: Update various dependencies to use updated gutenberg packages. -+ Bug fix: Fixed a WordPress 5.3 issues with JSON data affecting the ability to save course/membership instructors. -+ Bug fix: Import `InspectorControls` from `wp.blockEditor` in favor of deprecated `wp.editor` -+ Bug fix: Automatically store course/membership instructor with `post_author` data when the post is created. -+ Bug fix: Pass style rules as camelCase. -+ 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. - - -v3.37.3 - 2019-12-03 --------------------- - -+ Added an action `llms_certificate_generate_export` to allow modification of certificate exports before being stored on the server. -+ Don't unslash uploaded file `tmp_name`, thanks [@pondermatic](https://github.com/pondermatic)! -+ TwentyTwenty Theme Support: Hide site header and footer, and set a white body background in single certificates. -+ Renamed setting field IDs to be unique for open/close wrapper fields on the engagements and account settings pages. -+ Removed redundant functions defined in the `LLMS_Settings_Page` class to reduce code redundancy in account and engagement setting page classes. -+ The `LLMS_Settings_Page` base class now automatically defines actions to save and output settings content. - - -v3.37.2 - 2019-11-22 --------------------- - -+ LifterLMS notices will now be displayed on pages defined as a Course or Membership sales page. -+ TwentyTwenty Theme: Updated to use `background-color` property instead of `background` shorthand when adding custom elements to style. -+ Added filter `llms_sessions_end_idle_cron_recurrence` to allow customization of the recurrence of the idle session cleanup cronjob. -+ Added filter `llms_quiz_is_open` to allow customization of whether or not a quiz is available to a student. -+ When adding an client-side tracking events to the always make sure the server-side verification nonce is always set on the storage object. -+ The Course/Membership filter on the main students reporting screen now correctly limits post results based on instructor access. - - -v3.37.1 - 2019-11-13 --------------------- - -+ TwentyTwenty Theme: Fixed course information block misalignment. -+ Fixed conflict with WooCommerce resulting from the movement of the deprecated LiftreLMS function `is_filtered()`. - - -v3.37.0 - 2019-11-11 --------------------- - -##### Updates - -+ Tested and compatible with WordPress core 5.3. -+ Add theme support for the TwentyTwenty core default theme. -+ Improved security and data sanitization in with regards to the SendWP integration connector. - -##### LifterLMS Rest API 1.0.0-beta.8 - -+ Added memberships controller, huge thanks to [@pondermatic](https://github.com/pondermatic)! -+ Added new filters: - - + `llms_rest_lesson_filters_removed_for_response` - + `llms_rest_course_item_schema` - + `llms_rest_pre_insert_course` - + `llms_rest_prepare_course_object_response` - + `llms_rest_course_links` - -+ Improved validation when defining instructors for courses. -+ Improved performance on post collection listing functions. -+ Ensure that a course instructor is always set for courses. -+ Fixed `sales_page_url` not returned in `edit` context. -+ 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. - - -v3.36.5 - 2019-11-05 --------------------- - -+ 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. - - -v3.36.4 - 2019-11-01 --------------------- - -+ Fixes a conflict with CartFlows introduced by a Divi theme compatibility fix added in 3.36.3. Is WordPress complicated or what? - - -v3.36.3 - 2019-10-24 --------------------- - -##### Updates - -+ Added new `LLMS_Membership` class methods: `get_categories()`, `get_tags()` and `toArrayAfter()` methods. Thanks [@pondermatic](https://github.com/pondermatic)! - -##### Compatibility - -+ Fixed access plan description conflicts with the Classic Editor block. This also resolves compatibility issues with Elementor which uses a hidden TinyMCE instance. -+ 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. - -##### Bugfixes - -+ Added translation to error message encountered when non-members attempt to purchase a members-only access plan. Thanks [@mrosati84](https://github.com/mrosati84)! -+ Fix return of `LLMS_Generator::set_generator()`. -+ Fixed a typo causing invalid imports from returning the expected error. Thanks [@pondermatic](https://github.com/pondermatic)! -+ Fixed issue preventing membership post type settings from saving properly due to incorrect sanitization filters. -+ Fixed issue where `wp_list_pluck()` would run on non arrays. - - -v3.36.2 - 2019-10-01 --------------------- - -##### Updates - -+ Tested to WordPress 5.3.0-beta.2 -+ Upgrade UI on student course reporting screens. -+ Added logic to physically remove from the membership level and remove enrollments data on related products, when deleting a membership enrollment. -+ Lesson metabox "start" drip method made available only if the parent course has a start date set. - -##### Bugfixes - -+ Fixed JS error when client-side event tracking settings aren't loaded, thanks [@wenchen](https://github.com/wenchen)! -+ Fixed PHP warning resulting from drip the "Course Start" lesson drip settings when no course start date exists. -+ Fixed fatal error encountered when reviewing an order placed with a payment gateway that's been deactivated. - -##### Files Updated - -+ assets/js/app/llms-tracking.js -+ includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php -+ includes/models/model.llms.lesson.php -+ includes/models/model.llms.student.php -+ lifterlms.php - -##### Templates Updated - -+ templates/admin/post-types/order-details.php -+ templates/admin/reporting/tabs/students/courses-course.php - - -v3.36.1 - 2019-09-24 --------------------- - -##### Updates - -+ Include SendWP Connector in LifterLMS Engagement Settings. -+ Removed usage of `WP_Error::has_errors()` to support WordPress version prior to 5.1. -+ Improve performances when checking if an event is valid in `LLMS_Events->is_event_valid()`. -+ Remove redundant check on `is_singular()` and `is_post_type_archive()` in `LLMS_Events->should_track_client_events()`. - -##### Bugfixes - -+ 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. -+ Fixed an issue allowing recurring charges to continue processing after the order or customer had been deleted from the site. -+ Fixed issue causing Membership Restriction settings from properly saving. -+ 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). -+ Fixed "Last Seen" column displaying wrong date when the student last login date was saved as timestamp. -+ Fixed an issue causing popover notifications to be skipped (never displayed) as a result of redirects. - - -v3.36.0 - 2019-09-16 --------------------- - -##### User Interaction event and session Tracking - -+ Added user interaction tracking for the following events: - - + User sign in and out. - + Page load and exit (for LMS content) - + Page focus and blur (for LMS content) - + And more to come - -+ Interaction events are grouped into sessions automatically. A session is "closed" after 30 minutes of inactivity or a log-out event. -+ Added "Last Seen" student reporting column which reports the last recorded activity for the student. - -##### Enhancements - -+ Automatically hydrate when calling LLMS_Abstract_Database_Store::to_array(). -+ Added CSS to make course and lesson video embeds automatically responsive. - -##### Bug Fixes - -+ Correctly pass the `$remember` variable when using `llms_set_person_auth_cookie()`. -+ Fixed undefined index error when retrieving an unset value from an unsaved database model. -+ Fix issue causing quotes to be encoded in shortcodes used in course and membership restriction message settings fields. -+ Fix issue preventing manual updates of order dates (next payment, trial expiration, and access expiration) from being saved properly. - - -v3.35.2 - 2019-09-06 --------------------- - -+ When sanitizing settings, don't strip tags on editor and textarea fields that allow HTML. -+ Added JS filter `llms_lesson_rerender_change_events` to lesson editor view re-render change events. - - -v3.35.1 - 2019-09-04 --------------------- - -+ Fix instances of improper input sanitization and handling. -+ Include scripts, styles, and images for reporting charts and datepickers - - -v3.35.0 - 2019-09-04 --------------------- - -##### Security Notice - -+ Fixed a security vulnerability disclosed by the WordPress plugin review team. Please upgrade immediately! - -##### Updates - -+ Explicitly setting css and js file versions for various static assets.. -+ Added data sanitization methods in various form handlers. -+ Added nonce verification to various form handlers. - -##### Bug fixes - -+ Fixed some translation strings that had literal variables instead of placeholders. -+ Fixed undefined index error encountered when attempting to email a voucher export. -+ Fixed undefined index error when PHP file upload errors are encountered during a course import. - -##### Deprecations - -The following unused classes have been marked as deprecated and will be removed from LifterLMS in the next major release. - -+ LLMS_Analytics_Memberships -+ LLMS_Analytics_Courses -+ LLMS_Analytics_Sales -+ LLMS_Meta_Box_Expiration -+ LLMS_Meta_Box_Video - -##### Template Updates - -+ [admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/courses/overview.php) -+ [admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/memberships/overview.php) -+ [admin/reporting/tabs/quizzes/attempts.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempts.php) -+ [admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/overview.php) -+ [admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses-course.php) -+ [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php) -+ [loop/featured-image.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/featured-image.php) -+ [myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php) -+ [quiz/results.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results.php) -+ [single-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/single-certificate.php) -+ [single-no-access.php](https://github.com/gocodebox/lifterlms/blob/master/templates/single-no-access.php) -+ [taxonomy-course_cat.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_cat.php) -+ [taxonomy-course_difficulty.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_difficulty.php) -+ [taxonomy-course_tag.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_tag.php) -+ [taxonomy-course_track.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_track.php) -+ [taxonomy-membership_cat.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-membership_cat.php) -+ [taxonomy-membership_tag.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-membership_tag.php) - - -v3.34.5 - 2019-08-29 --------------------- - -+ Fixed logic issues preventing pending orders from being completed. - -##### Templates Changed - -+ [checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php) - -v3.34.4 - 2019-08-27 --------------------- - -+ Add a new admin settings field type, "keyval", used for displaying custom html alongside a setting. -+ Added filter `llms_order_can_be_confirmed`. -+ Always bind JS for the login form handler on checkout and registration screens. - -##### Templates Changed - -+ [checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php) - -##### LifterLMS REST API v1.0.0-beta.6 - -+ Fix issue causing certain webhooks to not trigger as a result of action load order. -+ Change "access_plans" to "Access Plans" for better human reading. - - -v3.34.3 - 2019-08-22 --------------------- - -+ During payment gateway order completion, use `llms_redirect_and_exit()` instead of `wp_redirect()` and `exit()`. - -##### LifterLMS REST API v1.0.0-beta.5 - -+ Load all required files and functions when authentication is triggered. -+ Access `$_SERVER` variables via `filter_var` instead of `llms_filter_input` to work around PHP bug https://bugs.php.net/bug.php?id=49184. - - -v3.34.2 - 2019-08-21 --------------------- - -##### LifterLMS REST API v1.0.0-beta.4 - -+ Load authentication handlers as early as possible. Fixes conflicts with numerous plugins which load user information earlier than expected by the WordPress core. -+ Harden permissions associated with viewing student enrollment information. -+ Returns a 400 Bad Request when invalid dates are supplied. -+ Student Enrollment objects return student and post id's as integers instead of strings. -+ Fixed references to an undefined function. - - -v3.34.1 - 2019-08-19 --------------------- - -+ Update LifterLMS REST to v1.0.0-beta.3 - -##### Interface and Experience improvements during API Key creation - -+ Better expose that API Keys are never shown again after the initial creation. -+ Allow downloading of API Credentials as a `.txt` file. -+ Add `required` properties to required fields. - -##### Updates - -+ Added the ability to CRUD webhooks via the REST API. -+ Conditionally throw `_doing_it_wrong` on server controller stubs. -+ Improve performance by returning early when errors are encountered for various methods. -+ Utilizes a new custom property `show_in_llms_rest` to determine if taxonomies should be displayed in the LifterLMS REST API. -+ On the webhooks table the "Delivery URL" is trimmed to 40 characters to improve table readability. - -##### Bug fixes - -+ Fixed a formatting error when creating webhooks with the default auto-generated webhook name. -+ On the webhooks table a translatable string is output for the status instead of the database value. -+ Fix an issue causing the "Last" page pagination link to display for lists with 0 possible results. -+ Don't output the "Last" page pagination link on the last page. - - - -v3.34.0 - 2019-08-15 --------------------- - -##### LifterLMS REST API v1.0.0-beta.1 - -+ A robust REST API is now included in the LifterLMS core. -+ Create API Keys to consume and manage LifterLMS resources and students from external applications. -+ Create webhooks to pass LifterLMS resource data to external applications (like Zapier!). -+ The full API specification can be found at [https://gocodebox.github.io/lifterlms-rest/](https://gocodebox.github.io/lifterlms-rest/). - -##### Student management capabilities - -+ Explicit capabilities have been added to determine which users can create, view, update, and delete students. -+ Admins and LMS Managers have all student management capabilities. -+ Instructors and instructors assistants are granted limited view capabilities allowing them to only view students enrolled in their own courses/memberships. -+ Added the `list_users` capability to the "Instructor" role, allowing instructor's to better view and manage their assistant instructors. -+ The new capabilities are: `create_students`, `view_students`, `view_others_students`, `edit_students`, `edit_others_students`, `delete_students`, & `delete_others_students`. - -##### Updates - -+ Added new actions to help differentiate enrollment creation and update events. -+ Added methods and logic for managing user management of other users. -+ Added a filter `llms_table_get_table_classes` to LifterLMS admin tables which allows customization of the CSS classes applied to the `` elements. Thanks [@pondermatic](https://github.com/pondermatic)! -+ Added a filter `llms_install_get_schema` to the database schema to allow 3rd parties to run table installations alongside the core. -+ Added the ability to pull "raw" (unfiltered) data from the database via classes extending the `LLMS_Post_Model` abstract. -+ Added a `bulk_set()` method to the `LLMS_Post_Model` abstract allowing the updating of multiple properties in one command. -+ Added `comment_status`, `ping_status`, `date_gmt`, `modified_gmt`, `menu_order`, `post_password` as gettable\settable post properties via the `LLMS_Post_Model` abstract. -+ Links on reporting tables are now the proper color. -+ 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). -+ Updated LifterLMS Blocks to 1.5.2 - -##### Bug Fixes - -+ Fixed an issue preventing the `user_url` property from being retrieved by the `get()` method of the `LLMS_Abstract_User_Data` class. -+ 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. -+ Fixed an issue which would allow LMS Managers to edit and delete site administrators. - -##### Deprecations - -**The following functions and methods have been marked as deprecated and will be removed from LifterLMS with the next major release.** - -+ LLMS_Course::get_children_sections() use LLMS_Course::get_sections( 'posts' )" instead -+ LLMS_Course::get_children_lessons() use LLMS_Course::get_lessons( 'posts' )" instead -+ LLMS_Course::get_author() -+ LLMS_Course::get_author_id() use LLMS_Course::get( "author" ) instead -+ LLMS_Course::get_author_name() -+ LLMS_Course::get_sku() use LLMS_Course::get( "sku" ) instead -+ LLMS_Course::get_id() use LLMS_Course::get( "id" ) instead -+ LLMS_Course::get_title() use get_the_title() instead -+ LLMS_Course::get_permalink() use get_permalink() instead -+ LLMS_Course::get_user_postmeta_data() -+ LLMS_Course::get_user_postmetas_by_key() -+ LLMS_Course::get_checkout_url() -+ LLMS_Course::get_start_date() use LLMS_Course::get_date( "start_date" ) instead -+ LLMS_Course::get_end_date() use LLMS_Course::get_date( "end_date" ) instead -+ LLMS_Course::get_next_uncompleted_lesson() -+ LLMS_Course::get_lesson_ids() use LLMS_Course::get_lessons( "ids" ) instead -+ LLMS_Course::get_syllabus_sections() use LLMS_Course::get_sections() instead -+ LLMS_Course::get_short_description() use LLMS_Course::get( "excerpt" ) instead -+ LLMS_Course::get_syllabus() use LLMS_Course::get_sections() instead -+ LLMS_Course::get_user_enroll_date() -+ LLMS_Course::get_user_post_data() -+ LLMS_Course::check_enrollment() -+ LLMS_Course::is_user_enrolled() use llms_is_user_enrolled() instead -+ LLMS_Course::get_student_progress() use LLMS_Student::get_progress() instead -+ LLMS_Course::get_membership_link() - - -v3.33.2 - 2019-06-26 --------------------- - -+ It is now possible to send test copies of the "Student Welcome" email to yourself. -+ Improved information logged when an error is encountered during an email send. -+ Add backwards compatibility for legacy add-on integrations priority loading method. -+ Fixed undefined index notice when viewing log files on the admin status screen. - - -v3.33.1 - 2019-06-25 --------------------- - -##### Updates - -+ Added method to retrieve the load priority of integrations. -+ The capabilities used to determine if uses can clone and export courses now check `edit_course` instead of `edit_post`. - -##### Bug Fixes - -+ Fixed an issue which would cause the "Net Sales" line to sometimes display as a bar on the sales revenue reporting chart. -+ Fixed an issue causing a PHP notice to be logged when viewing the sales reporting screen. -+ Fixed an issue causing backslashes to be added before quotation marks in access plan descriptions. -+ Integration classes are now loaded in the order defined by the integration class. -+ Fixed an issue causing a PHP error when viewing the admin logs screen when no logs exist. - - -v3.33.0 - 2019-05-21 --------------------- - -##### Updates - -+ Added the ability for site administrators to delete (completely remove) enrollment records from the database. -+ 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)! -+ Hooks in the dashboard order review template now pass the `LLMS_Order`. - -##### LifterLMS Blocks - -+ Updated to version 1.5.1 -+ All blocks are now registered only for post types where they can actually be used. -+ 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. - -##### Bug Fixes - -+ If an enrolled student accesses checkout for a course/membership they're already enrolled in they will be shown a message stating as much. -+ Removed a redundant check for the existence of an order on the dashboard order review template. -+ 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. -+ Fix issue causing errors when using the `[lifterlms_lesson_mark_complete]` shortcode on course post types. -+ Fixed an issue causing quiz questions to generate publicly accessible permalinks which could be indexed by search engines. - -##### Templates Changed - -+ [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) -+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/templates/myaccount/view-order.php) - - -v3.32.0 - 2019-05-13 --------------------- - -##### Updates - -+ Added Membership reporting -+ Added the ability to restrict coupons to courses and memberships which are in draft or scheduled status. -+ When recurring payments are disabled, output a "Staging" bubble on the "Orders" menu item. -+ Recurring recharges now add order notes and trigger actions when gateway or recurring payment status errors are encountered. -+ 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. -+ Updated the Action Scheduler library to the latest version (2.2.5) -+ Exposed the Action Scheduler's scheduled actions interface as a tab on the LifterLMS Status page. - -##### LifterLMS Blocks - -+ Updated to version 1.4.1. -+ Fixed issue causing asset paths to have invalid double slashes. -+ Fixed issue causing frontend css assets to look for an unresolvable dependency. - -##### Bug Fixes - -+ Fixed an issue allowing instructors to view a list of students from courses and memberships they don't have access to. -+ 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. - - -v3.31.0 - 2019-05-06 --------------------- - -##### Updates - -+ Tested to WordPress 5.2 -+ Adds explicit support for the twentynineteen default theme. -+ The main students reporting table can now be filtered to show only students enrolled in a specific course or membership. -+ Resolve conflict with WooCommerce (3.6 and later) resulting in 404s on the dashboard endpoints "lost password", "order history", and "edit account". -+ Adds a dynamic filter (`llms_notification_view{$trigger_id}_basic_options`) to basic (pop-over) notifications to allow configuration of their settings. -+ The filter `llms_plan_get_checkout_url` now passes a 3rd parameter: `$check_availability` -+ Improves `LLMS_Course_Data` and `LLMS_Quiz_Data` classes by adding shared functionality to a shared abstract, `LLMS_Abstract_Post_Data` -+ Changed access on class methods in `LLMS_Shortcode_Courses` from private to protected, thanks [@andrewvaughan](https://github.com/andrewvaughan)! - -##### Bug fixes - -+ 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. -+ Fix an issue allowing access plan sales prices to be set as negative values. - -##### LifterLMS Blocks - -+ Updated to LifterLMS Blocks 1.4.0. -+ 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. -+ 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. - -##### Deprecations - -+ `LLMS_Query::add_query_vars()` use `LLMS_Query::set_query_vars()` instead. - - -v3.30.3 - 2019-04-22 --------------------- - -##### Updates - -+ Fixed typos and spelling errors in various strings. -+ Corrected a typo in the `content-disposition` header used when exporting voucher CSVs, thanks [@pondermatic](https://github.com/pondermatic)! -+ 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)! -+ Removed commented out code on the Student Dashboard Notifications Tab template, thanks [@tnorthcutt](https://github.com/tnorthcutt)! - -##### Bug Fixes - -+ Renamed "descrpition" key to "description" found in the return of `LLMS_Instructor()->toArray()`. -+ Fixed an issue causing slashes to be stripped from course content when cloning a course. -+ 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)! -+ Fixed an undefined variable notice encountered when filtering quiz attempts on the quiz attempts reporting screen, thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)! -+ 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)! -+ [@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. - -##### Deprecations - -The following unused classes have been marked as deprecated and will be removed from LifterLMS in the next major release. - -+ `LLMS\Users\User` -+ `LLMS_Analytics_Page` -+ `LLMS_Course_Basic` -+ `LLMS_Lesson_Basic` -+ `LLMS_Quiz_Legacy` - -##### Template Updates - -+ [templates/myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php) - - -v3.30.2 - 2019-04-09 --------------------- - -+ 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. -+ Added hooks and filters to the `LLMS_Generator` class to allow 3rd parties to easily generate content during course clone and import operations. -+ 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. -+ Fixed a PHP undefined variable warning present on the payment confirmation screen. - -##### Template Updates - -+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php) - - -v3.30.1 - 2019-04-04 --------------------- - -##### Updates - -+ Added handler to automatically resume pending (incomplete or abandoned) orders. -+ Classes extending the `LLMS_Abstract_API_Handler` can now prevent a request body from being sent. -+ 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). -+ Remove unused CSS code on the admin panel. - -##### Bug Fixes - -+ Fixed a bug preventing course imports as a result of action priority ordering issues. -+ 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. -+ 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. -+ Increased the specificity of LifterLMS element tooltip hovers. Resolves a conflict causing issues on the WooCommerce tax rate management screen. -+ 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)! -+ Fixed builder deep links to quizzes freezing due to dependencies not being available during initialization. -+ Fixed builder issue causing duplicate copies of questions to be added when adding existing questions multiple times. - -##### Template Updates - -+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php) - - -v3.30.0 - 2019-03-21 --------------------- - -##### Updates - -+ **Create custom thank you pages with new access plan checkout redirect options.** -+ Added the ability to sort items on the membership auto enrollment table (drag and drop to sort and reorder). -+ Improved the interface and interactions with the membership auto enrollment table settings. - -##### LifterLMS Blocks - -+ Updated LifterLMS Blocks to 1.3.8. -+ Fixed an issue causing some installations to be unable to use certain blocks due to jQuery dependencies being declared improperly. - -##### Bug Fixes - -+ Fixed issue preventing courses with the same title from properly displayed on the membership automatic enrollment courses table on the admin panel. -+ Fixed an issue preventing builder custom fields from being able to specify a custom sanitization callback. -+ Fixed an issue preventing builder custom fields from being able to properly save and render multi-select data. - -##### Template Updates - -+ [templates/product/access-plan-restrictions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-restrictions.php) -+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php) - - -v3.29.4 - 2019-03-08 --------------------- - -+ Fixed an issue preventing users with email addresses containing an apostrophe from being able to login. - - -v3.29.3 - 2019-03-01 --------------------- - -##### Bug Fixes - -+ Removed attempts to validate & save access plan data when the Classic Editor "post" form is submitted. -+ Fix issue causing 1-click free-enrollment for logged in users to refresh the screen without actually performing an enrollment. - -##### Template Updates - -+ [product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php) - - -v3.29.2 - 2019-02-28 --------------------- - -+ Fix issue causing blank "period" values on access plans from being updated. -+ Fix an issue preventing paid access plans from being switched to "Free". - - -v3.29.1 - 2019-02-27 --------------------- - -+ Automatically reorder access plans when a plan is deleted. -+ Skip (don't create) empty plans passed to the access plan save method as a result of deleted access plans. - - -v3.29.0 - 2019-02-27 --------------------- - -##### Improved Access Plan Management - -+ Added a set of methods for creating access plans programmatically. -+ Updated the Access Plan metabox on courses and lessons with improved data validation. -+ 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). -+ Access plans are now created and updated via AJAX requests, resolves a 5.0 editor issue causing duplicated access plans to be created. - -##### Student Management Improvements - -+ Added the ability for instructors and admins to mark lessons complete and incomplete for students via the student course reporting table. - -##### Admin Panel Settings and Reporting Design Changes - -+ Replaced LifterLMS logos and icons on the admin panel with our new logo LifterLMS Logo and Icons. -+ Revamped the design and layout of settings and reporting screens. - -##### Checkout Improvements - -+ Updated checkout javascript to expose an error addition functions -+ Abstracted the checkout form submission functionality into a callable function not directly tied to `$_POST` data -+ Removed display order field from payment gateway settings in favor of using the gateway table sortable list - -##### Other Updates - -+ Removed code related to an incompatibility between Yoast SEO Premium and LifterLMS resulting from former access plan save methods. -+ Reduced application logic in the `course/complete-lesson-link.php` template file by refactoring button display filters into functions. -+ Added function for checking if request is a REST request -+ Updated LifterLMS Blocks to version 1.3.7 - -##### Bug Fixes - -+ 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. -+ Fixed the checkout nonce to have a unique ID & name -+ Fixed an issue with deleted quizzes causing quiz notification's to throw fatal errors. -+ Fixed an issue preventing notification timestamps from displaying on the notifications dashboard page. -+ Fix an issue causing `GET` requests with no query string variables from causing issues via incorrect JSON encoding via the API Handler abstract. -+ Fix an issue causing access plan sale end dates from using the default WordPress date format settings. -+ `LLMS_Lesson::has_quiz()` will now properly return a boolean instead of the ID of the associated quiz (or 0 when none found) - -##### Template Updates - -+ [checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php) -+ [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) -+ [product/access-plan-pricing.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-pricing.php) -+ [notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/master/templates/notifications/basic.php) - -##### Templates Removed - -Admin panel templates replaced with view files which cannot be overridden from a theme or custom plugin. - -+ `admin/post-types/product-access-plan.php` -+ `admin/post-types/product.php` - - -v3.28.3 - 2019-02-14 --------------------- - -+ ❤❤❤ Happy Valentines Day or whatever ❤❤❤ -+ Tested to WordPress 5.1 -+ 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. - - -v3.28.2 - 2019-02-11 --------------------- - -##### Updates - -+ Updated default country list to remove non-existent countries and resolve capitalization issues, thanks [nrherron92](https://github.com/nrherron92)! - -##### Bug fixes - -+ Fixed an issue causing the email notification content getter to use the same filter as popover notifications. -+ 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. -+ Fixed an issue causing 404s on paginated dashboard endpoints when the permalink structure is set to anything other than `%postname%`. - -##### Deprecations - -+ `LLMS_Query->set_dashboard_pagination()` - - -v3.28.1 - 2019-02-01 --------------------- - -+ Fixed an issues preventing exports to be accessible on Apache servers. -+ Fixed an issue causing servers with certain nginx rules to open CSV exports directly instead of downloading them. - - -v3.28.0 - 2019-01-29 --------------------- - -##### Updates - -+ 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. -+ Updated lesson metabox to use icons for attached quizzes -+ Added an orange highlight to the admin "Add-Ons & More" menu item -+ Removed unused cron event. - -##### LifterLMS Blocks - -+ Updated LifterLMS Blocks to 1.3.4 -+ Adds support for handling courses & lessons in "Classic Editor" mode as defined by the Divi page builder -+ Skips course and lesson migration when "Classic" mode is enabled. -+ Adds conditions to identify "Classic" mode when the Classic Editor plugin settings are configured to enforce classic (or block) mode for *all* posts. - -##### Database Updates - -+ Unschedules the aforementioned unused cron event. - -##### Bug fixes - -+ Fixed an issue preventing the temp directory old file cleanup cron from firing on schedule. -+ During plugin uninstallation the tmp cleanup cron will now be properly unscheduled. -+ Fixed an issue causing notifications on the student dashboard to appear on top of static headers or the WP Admin Bar when scrolling. -+ Fixed an issue preventing manual updating of customer and source information on orders resulting from unfocusable hidden form fields. -+ Fixed mismatched HTML tags on the Admin Add-Ons screen - -##### Deprecations - -+ Class method: `LLMS_Admin_Table::queue_export()` -+ Class: `LLMS_Processor_Table_To_Csv` - - -v3.27.0 - 2019-01-22 --------------------- - -###### Updates - -+ 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. -+ Added the ability to detach questions from quizzes. Coupled with adding existing questions, questions can now be easily moved between quizzes. -+ Added permalink capabilities to the builder to allow linking to specific items within the builder (a lesson, quiz, etc...). -+ Quizzes with 0 possible points will no longer show a Pass/Fail chart with a 0% (failing) grade on quiz results screens. -+ 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. - -##### Bug Fixes - -+ 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)! -+ Fixed an issue causing quotation marks in quiz question answers to show escaping slashes on results screens. -+ Fixed a bug preventing viewing quiz results for quizzes with questions that have been deleted. -+ Fixed a bug causing a PHP Notice to be output when registering a new user with a valid voucher. - -##### Templates Changed - -+ [quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt.php) - - -v3.26.4 - 2019-01-16 --------------------- - -+ 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. - - -v3.26.3 - 2019-01-15 --------------------- - -##### Updates - -+ Fix issue preventing course difficulty and course length from being edited when using the classic editor plugin. -+ Improved pagination methods on Student Dashboard Endpoints -+ "My Notifications" dashboard tab now consistently paginated like other dashboard endpoints -+ Update to [LifterLMS Blocks 1.3.1](https://make.lifterlms.com/2019/01/15/lifterlms-blocks-version-1-3-1/). - -##### Bug Fixes - -+ Fixed an issue preventing course difficulty and course length from being edited when using various page builders. -+ Fixed issues causing errors on quiz reporting screens for quiz attempts made by deleted users. - -##### Deprecated Functions - -+ `LLMS_Student_Dashboard::output_notifications_content()` replaced with `lifterlms_template_student_dashboard_my_notifications()` - -##### Templates Changed - -+ [myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php) -+ [admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempt.php) - - -v3.26.2 - 2019-01-09 --------------------- - -+ Fast follow to fix incorrect version number pushed to the readme files for 3.26.1 which prevents upgrading to 3.26.1 - - -v3.26.1 - 2019-01-09 --------------------- - -##### Updates - -+ Tested to WordPress 5.0.3 -+ Student CSV reports will now bypass cached data during report generation. -+ Add course and membership catalog visibility settings into the block editor. -+ Includes LifterLMS Blocks 1.3.0. - -##### Bug Fixes - -+ Fixed issue preventing the course instructors metabox from displaying when using the classic editor plugin. -+ Fixed an issue causing membership background enrollment from processing when the course background processor is disabled via filters. -+ Fixed an issue causing errors when reviewing orders on the admin panel which were placed via a payment gateway which is no longer active. -+ Fixed an issue preventing course difficulty and course length from being edited when using the classic editor plugin. -+ Fixed a very convoluted conflict between LifterLMS, WooCommerce, and Elementor explained at https://github.com/gocodebox/lifterlms/issues/730. - - -v3.26.0 - 2018-12-27 --------------------- - -+ Adds conditional support for page builders: Beaver Builder, Divi Builder, and Elementor. -+ Fixed issue causing LifterLMS core sales pages from outputting automatic content (like pricing tables) on migrated posts. -+ Student unenrollment calls always bypass cache during enrollment precheck. -+ Membership post type "name" label is now plural (as it is supposed to be). - - -v3.25.4 - 2018-12-17 --------------------- - -+ 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. -+ Added a filter (`llms_dashboard_courses_wp_query_args`) to the WP_Query used to display courses on the student dashboard. -+ Fixed issue on course builder causing prerequisites to not be saved when the first lesson in a course was selected as the prereq. -+ Fixed issue on course builder causing lesson settings to be inaccessible without first saving the lesson to the database. - - -v3.25.3 - 2018-12-14 --------------------- - -+ Fixed compatibility issue with the Classic Editor plugin when it was added after a post was migrated to the new editor structure. - - -v3.25.2 - 2018-12-13 --------------------- - -+ Added new filters to the `LLMS_Product` model. -+ Fix issue with student dashboard login redirect causing a white screen on initial login. - - -v3.25.1 - 2018-12-12 --------------------- - -##### Updates - -+ Editor blocks now display a lock icon when hovering/selecting a block which corresponds to the enrollment visibility settings of the block. -+ Removal of core actions is now handled by a general migrator function instead of by individual blocks. - -##### Bug fixes - -+ Fixed issue preventing strings from the lifterlms-blocks package from being translatable. -+ Fix issue causing block visibility options to not be properly set when enrollment visibility is first enabled for a block. -+ Fixed compatibility issue with Yoast SEO Premium redirect manager settings, thanks [@moorscode](https://github.com/moorscode)! -+ Fixed typo preventing tag size options (or filters) of course information block from functioning properly. Thanks [@tnorthcutt](https://github.com/tnorthcutt)! - -##### Templates Changed - -+ [templates/course/meta-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/meta-wrapper-start.php) - - -v3.25.0 - 2018-12-05 --------------------- - -##### WordPress 5.0 Ready! - -+ **Tested with WordPress core 5.0 (Gutenberg)!** -+ Editor Blocks: Course and Lesson layouts are now (preferably) powered by various editor blocks. -+ When a block is added to a course or lesson, the template hook that automatically outputs that element is removed automatically (preventing duplicates). -+ If you use the LifterLMS Labs: Action Manager you may no longer need it! -+ Course & Membership instructors are now managed through an editor "plugin". Check out the rocket icon near the "Publish/Update" button. -+ Instructor metabox will load conditionally based on presence of the block editor -+ New courses and lessons will automatically have a preloaded block editor template -+ Courses and lessons will automatically be "migrated" to these templates when edited on the admin panel -+ Various course settings conditionally load based on the presence of the block editor -+ Added filter to the headline size in the `course/meta-wrapper-start.php` template. Allows customization of headline via the "Course Information" block settings. -+ If you're not ready for WordPress 5.0 you can still upgrade LifterLMS. This release is fully functional without the block editor. - -##### Bug Fixes - -+ Fixed typo in `quiz/start-button.php` template. -+ Fixed error occurring during activation of LaunchPad via the Add-Ons & More screen. -+ Fixed issue causing quiz reporting screens to be blank for users without `view_others_lifterlms_reports` capabilities. - -##### Templates Changed - -+ [templates/course/author.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/author.php) -+ [course/meta-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/meta-wrapper-start.php) -+ [quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/start-button.php) - - -v3.24.3 - 2018-11-13 --------------------- - -##### Updates - -+ 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)! - -##### Bug Fixes - -+ Fixed issue causing fatal errors encountered during certificate downloading caused by CSS `` tags existing outside of the `` element. -+ Certificates downloaded by users who can see the WP Admin Bar will no longer show the admin bar on the downloaded certificate -+ Fixed issue on iOS Safari causing multiple choice quiz questions to require a "long press" to be properly selected -+ 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)! -+ Ensure that fallback url slugs for course & membership archives are translatable. - - -v3.24.2 - 2018-10-30 --------------------- - -+ Fix issue causing newline characters to be malformed on course builder description fields, resulting in `n` characters being output in strange places. - - -v3.24.1 - 2018-10-29 --------------------- - -##### Updates - -+ 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)! -+ 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)! - -##### Bug Fixes - -+ Fixed a typo in engagement drop creation dropdown. Thanks [README1ST](https://github.com/README1ST)! -+ Fixed issue causing backslash characters (`\`) to be removed from course elements (sections, lessons, quizzes, and assignments) constructed in the course builder. -+ 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. -+ 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. - -##### Templates Changed - -+ templates/checkout/form-confirm-payment.php - - -v3.24.0 - 2018-10-23 --------------------- - -##### "My Grades" Student Dashboard Endpoint - -+ A new student dashboard endpoint, "My Grades", has been added -+ 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 -+ Students can drill into individual reporting screens for each course where specific details for each course are available for review - -##### Grading Enhancements - -+ Each lesson can now be assigned an individual "points" value -+ 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 -+ Lessons can also be assigned a value of "0" to allow a lesson to not count towards the overall grade of the course. -+ Email notifications are now sent to a student when an instructor reviews, grades, or leaves remarks on a quiz attempt. - -##### Test Email Notifications - -+ An interface and API for sending test email notifications has been added, the following notifications can now be tested: - - + Purchase Receipt - + Quizzes: Failed (Thanks [@philwp](https://github.com/philwp)!) - + Quizzes: Graded - + Quizzes: Passed (Thanks [@philwp](https://github.com/philwp)!) - -##### Updates and Enhancements - -+ Quiz Passed & Quiz Failed notifications have new names on the admin panel ("Quizzes: Quiz Passed" & "Quizzes: Quiz Failed") -+ 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. -+ 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)! -+ In the course builder when a lesson is duplicated, the attached quiz will be duplicated as well -+ Minor increase to performance in the `LLMS_Course->get_lessons()` method -+ Added `student_id` as a parameter passed to the `llms_student_get_progress` filter -+ Updated all access plan templates added in 3.23.0 to ensure `ABSPATH` is defined to prevent direct template access -+ 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 -+ Refactored the `LLMS_Section->get_percent_complete()` method to utilize methods from the `LLMS_Student` model -+ Added the ability for admin table classes to define `` element CSS classes -+ Admin settings pages with no settings to save (like the Notifications list) no longer display a "Save" button -+ Added actions when creating, updating, and deleting records managed by `LLMS_Abstract_Database_Store` classes -+ Updated system report to include URLs to settings with URLs, adds a small speed boost to support request turn around time. - -##### Please Rate & Review LifterLMS on WordPress.org - -+ Added a WordPress.org review request link to the footer of LifterLMS admin pages. -+ Added a WordPress.org review request notice which displays a week after installation if the site has 50+ active students. - -##### Bug fixes - -+ Fixed issue causing HTML entity codes to display in email subject lines. Thanks [@philwp](https://github.com/philwp)! -+ Fixed issue causing post cleanup functions to run queries against unsupported post types. -+ Fixed typos in a handful of i18n functions so that the proper textdomain is now being used -+ Removed `get_option()` call to unused option `lifterlms_logout_endpoint` which ran on WordPress initialization unnecessarily. -+ Removed 3.21.0 fixes for iOS touch issues that are now causing iOS touch issues on quizzes. -+ 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) -+ 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 - -##### Deprecated Functions & Methods - -+ Deprecated `LLMS_Section->get_children_lessons()`, use `LLMS_Section->get_lessons( 'posts' )` instead - -##### Template Updates - -+ [course/syllabus.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/syllabus.php) -+ [product/access-plan-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-button.php) -+ [product/access-plan-description.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-description.php) -+ [product/access-plan-feature.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-feature.php) -+ [product/access-plan-pricing.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-pricing.php) -+ [product/access-plan-restrictions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-restrictions.php) -+ [product/access-plan-title.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-title.php) -+ [product/access-plan-trial.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-trial.php) -+ [product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php) - - -v3.23.0 - 2018-08-27 --------------------- - -##### Access Plan & Pricing Table Template Improvements - -+ 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! -+ New action hooks are available to modify the rendering of access plans in course / membership pricing tables. - - + `llms_access_plan`: Main hook for outputting an entire access plan within the pricing table - + `llms_before_access_plan`: Called before main content of access plan. Outputs the "Featured" area of plans - + `llms_acces_plan_content`: Main access plan content. Outputs title, pricing info, restrictions, and description - + `llms_acces_plan_footer`: Called after main content. Outputs trial info and the checkout / enrollment button - -+ Added filters to the returns of many of the functions in the `LLMS_Acces_Plan` model. -+ Minor improvements made to `LLMS_Access_Plan` model - -##### Updates and Enhancements - -+ Improved handling of empty blank / empty data when adding instructors to courses and memberships -+ 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 -+ Added filters to the saving of access plan data -+ Improved the HTML and added CSS classes to the access plan admin panel html view - -##### Bug Fixes - -+ Fixes issue causing the "Preview Changes" button on courses to lock the "Update" publishing button which prevents changes from being properly saved.gi -+ Fixed issue causing PHP errors when viewing courses / memberships on the admin panel when an instructor user was deleted -+ Fixed issue causing PHP notices when viewing course / membership post lists on the admin panel when an instructor user was deleted -+ Fixed issue causing PHP warnings to be generated when viewing the user add / edit screen on the admin panel -+ 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.* - -##### Template Updates - -+ [templates/admin/post-types/product-access-plan.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/product-access-plan.php) -+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/pricing-table.php) - - -v3.22.2 - 2018-08-13 --------------------- - -+ Fixed issue causing banners on general settings screen to cause a fatal error when api connection errors occurred -+ Improved CSS on setup wizard - - -v3.22.1 - 2018-08-06 --------------------- - -+ Fix issue causing themes to appear as requiring updates when using the LifterLMS Helper - - -v3.22.0 - 2018-07-31 --------------------- - -+ 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). -+ Course and membership catalogs items in navigation menus will now have expected CSS classes to identify current item and current item parents -+ The admin panel add-ons screen has been reworked to be powered by the lifterlms.com REST api -+ Some visual changes have been made to the add-ons screen -+ The colors on the voucher screen on the admin panel have been updated to match the rest of the interfaces in LifterLMS - - -v3.21.1 - 2018-07-24 --------------------- - -+ Fixed issue causing visual issues on checkout summary when using coupons which apply discounts to a plan trial -+ Fixed issue causing `.mo` files stored in the `languages/lifterlms` safe directory from being loaded before files stored in the default location `languages/plugins` -+ Added methods to integration abstract to allow integration developers to automatically describe missing integration dependencies -+ Tested to WordPress 4.9.8 - -##### Template Updates - -+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-summary.php) - - -v3.21.0 - 2018-07-18 --------------------- - -##### Updates and Enhancements - -+ Added new actions before and after global login form HTML: `llms_before_person_login_form` & `llms_after_person_login_form` -+ Settings API can now create disabled fields -+ Added new actions to the checkout form: `lifterlms_pre_checkout_form` && `lifterlms_post_checkout_form` -+ Added CRUD functions for interacting with data located in the `wp_lifterlms_user_postmeta` table -+ Replaced various database queries for CRUD user postmeta data with new CRUD functions -+ Added new utility function to allow splicing data into associative arrays - -##### Bug Fixes - -+ 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 -+ Fixed plugin compatibility issue with Advanced Custom Fields -+ Fixed issue causing multiple choice quiz questions to require a double tap on some iOS devices -+ Fixed incorrectly named filter causing section titles to not display on student course reporting screens -+ 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. - -##### Template Updates - -+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php) -+ [templates/global/form-login.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-login.php) - - -v3.20.0 - 2018-07-12 --------------------- - -+ Updated user interfaces on admin panel for courses and memberships with relation to "Enrolled" and "Non-Enrolled" student descriptions -+ "Enrolled Student Description" is now the default WordPress editor -+ "Non-Enrolled Student Description" is now the "Sales Page" -+ Additional options for sales pages (the content displayed to visitors and non-enrolled students) have been added: - + Do nothing (show course description) - + Show custom content (use a WYSIWYG editor to define content) - + Redirect to a WordPress page (use custom templates and enhance page builder compatibility and capabilities) - + Redirect to a custom URL (use a sales page hosted on another domain!) -+ Tested to WordPress 4.9.7 - -v3.19.6 - 2018-07-06 --------------------- - -+ Fix file load paths in OptimizePress plugin compatibility function - - -v3.19.5 - 2018-07-05 --------------------- - -+ Fixed bug causing `select2` multi-selects from functioning as multi-selects -+ Fixed visual issue with `select2` elements being set without a width causing them to be both too small and too large in various scenarios. -+ Fixed duplicate action on dashboard section template - -##### Template Updates - -+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php) - - -v3.19.4 - 2018-07-02 --------------------- - -##### Updates and enhancements - -+ 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/) -+ Added event on builder to allow integrations to run trigger events when course elements are saved -+ 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) -+ Added new action called before validation occurs for a user account update form submission: `llms_before_user_account_update_submit` -+ Removed placeholders from form fields. Fixes a UX issue causing registration forms to appear cluttered due to having both placeholders and labels. - -##### Bug fixes - -+ Fixed issue allowing nonce checks to be bypassed on login and registration forms -+ Fixed issue causing a PHP notice if the registration form is submitted without an email address and automatic username generation is enabled -+ Fixed issue preventing email addresses with the "'" character from being able to register, login, or update account information -+ Fixed typo in automatic username generation filter `lifterlms_generated_username` (previously was `lifterlms_gnerated_username`) -+ Fixed issue causing admin panel static assets to have a double slash (//) in the asset URI path -+ 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. -+ 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. - - -v3.19.3 - 2018-06-14 --------------------- - -+ Fix issue causing new quizzes to be unable to load questions list without reloading the builder - - -v3.19.2 - 2018-06-14 --------------------- - -##### Updates and enhancements - -+ 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. -+ The currently viewed lesson will now be bold in the lesson outline widget. -+ 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. -+ Added the ability to filter quiz attempt reports by quiz status -+ 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. - -##### Bug fixes - -+ Fixed issue preventing oEmbed media from being used in quiz question descriptions -+ Fixed issue preventing `` from being used in quiz question descriptions -+ Quiz results will now exclude questions with 0 points value when displaying the number of questions in the quiz. -+ 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 -+ Fixed filter `lifterlms_reviews_section_title` which was unusable due to the incorrect usage of `_e()` within the filter. Now using `__()` as expected. -+ Fixed issue causing course featured image to display in place of lesson feature images - -##### Template Updates - -+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/lesson-preview.php) -+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php) -+ [templates/quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt.php) - - -v3.19.1 - 2018-06-07 --------------------- - -+ Fixed CSS specificity issue on admin panel causing white text on white background on system status pages - - -v3.19.0 - 2018-06-07 --------------------- - -##### Updates and enhancements - -+ Added a "My Memberships" tab to the student dashboard -+ "My Memberships" preview area -+ Updated admin panel order status badges to match frontend order status badges -+ 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. -+ When a student cancels an active recurring order from the student dashboard, the order will move to "Pending Cancellation" instead of "Cancelled" -+ Students can re-activate an order that's Pending Cancellation moving the expiration date to the next payment due date -+ Added the ability to edit the access expiration date for orders with limited access settings and for orders in the "pending-cancel" state -+ Added a filter to allow customization of the URL used to generate certificate downloads from -+ 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. -+ Added a filter (`llms_archive_description`) to allow filtering of the archive description -+ 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! -+ Updated the function used to check if lessons have featured images to improve performance and resolve an incompatibility issue with WP Overlays plugin. - -##### Bug fixes - -+ Fixed issue causing "My Courses" title to be duplicated on the student dashboard when viewing the endpoint -+ Fixed issue causing the trial price to be displayed with a strike-through during a sale -+ 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 -+ Fixed issue causing CSS rules to lose their declared order during exports causing export rendering issues with certain themes and plugin combinations - -##### Template Updates - -+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-summary.php) -+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-switch-source.php) -+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/lesson-preview.php) -+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php) - - -v3.18.2 - 2018-05-24 --------------------- - -+ Improved integrations settings screen to allow each integration to have it's own settings tab (page) with only its own settings -+ Allow programmatic access to notification content when notification views are accessed via filters -+ Fixed issue causing subscription cancellation notifications to be sent to admins when new orders were created -+ Fixed warning message displayed prior to membership bulk enrollment -+ Fixed multibyte character encoding issue encountered during certificate exports - - -v3.18.1 - 2018-05-18 --------------------- - -+ Attached `llms_privacy_policy_form_field()` and `llms_agree_to_terms_form_field()` to an action hook `llms_registration_privacy` -+ Define minimum WordPress version requirement as 4.8. - -##### Template Updates - -+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php) -+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-registration.php) - - -v3.18.0 - 2018-05-16 --------------------- - -##### Privacy & GDPR Compliance Tools - -+ Added privacy policy notice on checkout, enrollment, and registration that integrates with the WP Core 4.9.6 Privacy Policy Page setting -+ Added settings to allow customization of the privacy policy and terms & conditions notices during checkout, enrollment, and registration -+ Added suggested Privacy Policy language outlining information gathered by a default LifterLMS site - -+ During a WordPress Personal Data Export request the following LifterLMS information will be added to the export - - + All personal information gathered from registration, checkout, and enrollment forms - + Course and membership enrollments, progress, and grades - + Earned achievements and certificates - + All order data - -+ During a WordPress Personal Data Erasure request the following LifterLMS information will be erased - - + All personal information gathered from registration, checkout, and enrollment forms - + Earned achievements and certificates - + All notifications for or about the user - + 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. - + If the "Remove Student LMS Data" setting is enabled, all student data related to course and membership activity will be removed - -+ All of the above relies on features available in WordPress core 4.9.6 - -##### Updates and Enhancements - -+ Tested up to WordPress 4.9.6 -+ 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. -+ Added a "My Certificates" tab to the Student Dashboard -+ Certificates can be downloaded as HTML files (available when viewing a certificate or from the certificate reporting screen on the admin panel) -+ Admins can now delete certificates and achievements from reporting screens on the admin panel -+ Added additional information to certificate and achievement reporting tables -+ Expanded widths of admin settings page setting names to be a bit wider and more readable -+ Now conditionally hiding some settings when they are no longer relevant -+ Added daily cron automatically remove files from the `LLMS_TMP_DIR` which are more that 24 hours old -+ Removed unused template `content-llms_membership.php` -+ Added initialization actions for use by integration classes - -##### Bug Fixes - -+ Fixed issue causing coupon reports to always display "1" regardless of actual number of coupons used -+ Fixed issue causing new posts created via the Course Builder to always be created for user_id #1 -+ Fixed issue causing "My Achievements" to display twice on the My Achievements student dashboard tab -+ Fixed issue preventing lessons from being completed when a quiz in draft mode was attached to the lesson -+ Fixed issue causing minified RTL stylesheets to 404 - -##### Template Updates - -+ [templates/admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php) -+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php) -+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/content-certificate.php) -+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-registration.php) -+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php) - - -v3.17.8 - 2018-05-04 --------------------- - -##### Updates and Enhancements - -+ Added admin email notification when student cancels a subscription -+ Quiz results will now display the question's description when reviewing results as a student and on the admin panel during grading -+ Add action hook fired when a student cancels a subscription (`llms_subscription_cancelled_by_student`) -+ 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. -+ Updated the notifications settings table to be more friendly to the human eye - -##### Bug Fixes - -+ Fix admin scripts enqueue order. Fixes issue preventing manual student enrollment selection from functioning properly in certain scenarios. -+ Shift + Enter when in a question choice field now adds a return as expected instead of exiting the field -+ When pasting into question choice fields HTML from RTF documents will be automatically stripped -+ Ensure certificates print with a white background regardless of theme CSS -+ Fix issue causing themes with `overflow:hidden` on divs from cutting certificate background images -+ Upon export completion unlock tables regardless of mail success / failure -+ 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 -+ Fix error occurring when all 3rd party integrations are disabled by filter, credit to [@Mte90](https://github.com/Mte90)! -+ Ensure `LLMS()->integrations()->integrations()` returns all integrations regardless of availability. -+ Updated `LLMS_Abstract_Options_Data` to have an option set method - -##### Template Updates - -+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php) - - -v3.17.7 - 2018-04-27 --------------------- - -+ Fix issue preventing assignments passing grade requirement from saving properly -+ Fix issue preventing builder toggle switches from properly saving some switch field data -+ Fix with "Launch Builder" button causing it to extend outside the bounds of its container -+ Fix issue with builder radio select fields during view rerenders -+ Course Outline shortcode (and widget) now retrieve parent course of the current page more consistently with other shortcodes -+ 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) - - -v3.17.6 - 2018-04-26 --------------------- - -+ Updated language on recurring orders with no expiration settings. Orders no longer say "Lifetime Access" and instead output no expiration information -+ Quiz editor on builder updated to be consistent visually and functionally to the lesson settings editor -+ Improved the builder field API to allow for radio element fields -+ Fix issue causing JS error on admin settings pages -+ Updated CSS for Certificates to be more generally compatible with theme styles when printed -+ Allow system print settings to control print layout for certificates by removing explicit landscape declarations -+ Now passing additional data to filters used to create custom columns on reporting screens -+ Remove unused JS files & Chosen JS library -+ 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. -+ Achievement and Certificate basic notifications now auto-dismiss after 10 seconds like all other basic notifications -+ 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/ -+ Increased default z-index on notifications to prevent notifications from being hidden behind floating / static navigation menus - - -##### Template Updates - -+ [templates/myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-orders.php) -+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php) - - -v3.17.5 - 2018-04-23 --------------------- - -##### Admin Settings Interface Improvements - -+ Improved admin settings page interface to allow for section navigation -+ Updated checkout setting pages to utilize a separate section (page) for each available payment gateway -+ 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 -+ Moved dashboard endpoints to a separate section on the accounts settings area -+ Updated CSS on settings page to have more regular spacing between subtitles and settings fields -+ Added a "View" button next to any admin setting post/page selection field to allow quick viewing of the selected post -+ Purchase page setting field is now ajax powered like all other page selection settings -+ Renamed dashboard settings section titles to be more consistent with language in other areas of LifterLMS -+ All dashboard endpoints now automatically sanitized to be URL safe - -##### Updates and Enhancements - -+ Dashboard endpoints can now be deregistered by setting the endpoint slug to be blank on account settings - -##### Bug Fixes - -+ Fix issue causing 404s for various script files when SCRIPT_DEBUG is enabled -+ Fix issue with audio & video embeds to prevent fallback to default post attachments -+ Fix issue causing student selection boxes to malfunction due to missing dependencies when loaded over slow connections - -##### Template Updates - -+ [templates/myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/navigation.php) - - -v3.17.4 - 2018-04-17 --------------------- - -+ Added core RTL language support -+ Fixed fatal error on student management tables resulting from deleted admin users who manually enrolled students -+ Added filter to allow 3rd parties to disable achievement dupchecking (`llms_achievement_has_user_earned`) -+ Added {student_id} merge code which can be utilized on certificates -+ Added merge code insert button to certificates editor -+ Added filter to allow 3rd parties to disable certificate dupchecking (`llms_certificate_has_user_earned`) -+ Added filter to allow 3rd parties to add custom merge codes to certificates (`llms_certificate_merge_codes`) -+ Fix restriction check issue for lessons with drip or prerequisites on course outline widget / shortcode -+ Bumped WP tested to version to 4.9.5 - -##### Template Updates - -+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) -+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php) - - -v3.17.3 - 2018-04-11 --------------------- - -+ Course and Membership instructor metabox search field now correctly states "Select an Instructor" instead of previous "Select a Student" -+ Added missing translation for "Select a Student" on admin panel student selection search fields -+ Fix issue causing reporting export CSVs to throw a SYLK interpretation error when opened in Excel -+ Fix issue causing drafted courses and memberships to be published when the "Update" button is clicked to save changes -+ Remove use of PHP 7.2 deprecated `create_function` -+ Fix errors resulting from quiz questions which have been deleted -+ Fix issue causing current date / time to display as the End Date for incomplete quiz attempts on quiz reporting screens - -##### Template Updates - -+ [templates/admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempt.php) -+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php) - - -v3.17.2 - 2018-04-09 --------------------- - -+ Fixed issue preventing lesson video and audio embeds from being *removed* when using the course builder settings editor -+ Fixed issue causing question images to lose the image source -+ 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. -+ Add "All Time" reporting to various reporting filters -+ Added API for builder fields to enable multiple select fields -+ Fix memory leak related to assignments rendering on course builder -+ Fix issue causing course progress and enrollment checks to incorrectly display progress data cached for other users -+ Lesson progression actions (Mark Complete & Take Quiz buttons) will now always display to users with edit capabilities regardless of enrollment status - -##### Template Updates - -+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) -+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php) - - -v3.17.1 - 2018-03-30 --------------------- - -+ Refactored lesson completion methods to allow 3rd party customization of lesson completion behavior via filters and hooks. -+ Remove duplicate lesson completion notice implemented. Only popover notifications will display now instead of popovers and inline messages. -+ Object completion will now automatically prevent multiple records of completion from being recorded for a single object. -+ Lesson Mark Complete button and lessons completed by quiz now utilizes a generic trigger to mark lessons as complete: `llms_trigger_lesson_completion`. -+ Removed several unused functions from frontend forms class -+ Moved lesson completion form controllers to their own class - -##### Templates updates - -+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) - - -v3.17.0 - 2018-03-27 --------------------- - -##### Builder Updates - -+ 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 -+ Added new audio and video status indicator icons for each lesson -+ Various status indicator icons will now have different icons in addition to different colors depending on their state -+ Replaced "pencil" icons that open the WordPress post editor with a small "WP" icon -+ Added several actions and filters to backend functions so that 3rd parties can hook into builder saves -+ 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. -+ Added prerequisite validation for lessons to prevent accidental impossible prerequisite creating (eg: Lesson 5 can never be a prerequisite for Lesson 4) -+ 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/). -+ Fixed issue causing changes made in "Text" mode on content editors wouldn't trigger save events -+ Fixed issue causing lesson prerequisites to not properly display on the course builder -+ Fixed CSS z-index issues related to builder field tooltip displays -+ Removed unused Javascript dependencies - -##### Bug Fixes - -+ Fixed typo on filter on quiz question image getter function - -##### Updates - -+ 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! -+ Added PHP Requires plugin header (5.6 minimum) -+ Added HTTP User Agent data to the system report -+ [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 - - -v3.16.16 - 2018-03-19 ---------------------- - -+ Fixed builder issue causing multiple question choices to be incorrectly selected -+ 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 -+ Fixed builder issue preventing quizzes from being deleted before they were persisted to the database -+ Fixed builder issue causing autosaves to interrupt typing and reset lesson and section titles -+ Fixed JS console error related to LifterLMS JS dependency checks - - -v3.16.15 - 2018-03-13 ---------------------- - -##### Quiz Results Improvements and fixes - -+ Improved quiz result user and correct answer handling functions for more consistent HTML output -+ Result answers (correct and user) will display as lists -+ image question types will display without bullets and will "float" next to each other -+ Fixed issue causing quiz results with multiple answers from outputting all HTMLS with no spaces between them - -##### Quiz Grading - -+ Fixed issue causing advanced reorder and reorder question types from being graded incorrectly in some scenarios -+ 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' )` - -##### Fixes - -+ 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 -+ Added options for themes to add layout support to quizzes where the custom field utilizes an underscore at the beginning of the field key -+ Fixed CSS issue causing blanks of fill in the blanks to not be visible on the course builder when using Chrome on Windows -+ Removed unnecessary `get_option()` call to unused option `lifterlms_permalinks` -+ Updated permissions required to see various LifterLMS post types to rely on `manage_lifterlms` capabilities as opposed to `manage_options` - + 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 - + Affected content types are: Orders, Coupons, Vouchers, Engagements, Achievements, Certificates, and Emails -+ Several references to an option removed in LifterLMS 3.0 still existed in the codebase and have now been removed. - + Option `lifterlms_course_display_banner` is no longer called or referenced - + Template function `lifterlms_template_single_featured_image()` has been removed - + Actions referencing `lifterlms_template_single_featured_image()` have been removed - + Template function `lifterlms_get_featured_image_banner()` has been removed - + Template `templates/course/featured-image.php` has been removed - -##### Templates updates - -+ [quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php) - - -v3.16.14 - 2018-03-07 ---------------------- - -+ Courses reporting table now includes courses with the "Private" status -+ Fixed issue causing some achievement notifications to be blank -+ Added tooltips to question choice add / delete icon buttons -+ Quiz results meta information elements now have unique CSS classes -+ Removed reliance PHP 7.2 deprecated function `create_function()` -+ Fixed invalid PHP 7.2 syntax creating a warning found on the setup wizard -+ Fixed undefined index error related to admin notices -+ Fixed untranslatable string on Users table ("No Memberships") -+ Fixed discrepancy between membership restrictions as presented to logged out users and logged in users who cannot access membership -+ Fixed FireFox and Edge issue causing changes to number inputs made via HTML5 input arrows from properly triggering save events - - -v3.16.13 - 2018-02-28 ---------------------- - -+ Hotfix: Only create quizzes on the builder if quizzes exist on the lesson - - -v3.16.12 - 2018-02-27 ---------------------- - -+ Quizzes can now be detached (removed from a lesson) or deleted (deleted from the lesson and the database) via the Course Builder -+ Improved question choice randomization to ensure randomized choices never display in their original order. -+ When a lesson is deleted, any quiz attached to the lesson will become an orphan -+ When a lesson is deleted, any lesson with this lesson as a prerequisite will have it's prerequisite data removed -+ When a quiz is deleted, all questions attached to the quiz will also be deleted -+ When a quiz is deleted, the lesson associated with the quiz will have those associations removed -+ Fixed grammar issue on restricted lesson tooltips when no custom message is stored on the course. -+ 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/). - - -v3.16.11 - 2018-02-22 ---------------------- - -+ Course import/exports and lesson duplication now carry custom meta data from 3rd party plugins and themes -+ Added course completion date column to Course reporting students list -+ Restriction checks made against a quiz will now properly cascade to the quiz's parent lesson -+ Fixed issue preventing featured images from being exported with courses and lessons -+ Fixed duplicate lesson issue causing quizzes to be double assigned to the old and new lesson -+ Fixed issue allowing blog archive to be viewed by non-members when sitewide membership is enabled -+ Fixed builder issue causing data to be lost during autosaves if data was edited during an autosave -+ Fixed builder issue preventing lessons from moving between sections when clicking the "Prev" and "Next" section buttons -+ Added actions to `LLMS_Generator` to allow 3rd parties to extend core generator functionality - - -v3.16.10 - 2018-02-19 ---------------------- - -+ Content added to the editor of course & membership catalog pages will now be output *above* the catalog loop -+ Fix issue preventing iframes and some shortcodes from working when added to a Quiz question description -+ Added new columns to the Quizzes reporting table to display Course and Lesson relationships -+ Improved the task handler of background updater to ensure upgrade functions that need to run multiple times can do so -+ Fixed JS Backup confirmation dialog on the background updater. -+ Add support for 32-bit systems in the `LLMS_Hasher` class -+ Fix issue causing HTML template content to be added to lessons when duplicating an existing lesson within the course builder - -##### 3.16.0 migration improvements - -+ Accommodates questions imported by 3rd party Excel to LifterLMS Quiz plugin. Fixes an issue where choices would have no correct answer designated after migration. -+ 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. -+ Fix an issue that caused duplicate quizzes or questions to be created when the "Taking too long?" link was clicked - - -v3.16.9 - 2018-02-15 --------------------- - -+ Fix issue causing error on student dashboard when reviewing an order with an access plan that was deleted. -+ Fixed spelling error on course metabox -+ Fixed spelling error on frontend quiz interface -+ Fixed issues with 0 point questions: - + Will no longer prevent quizzes from being automatically graded when a 0 point question is in an otherwise automatically gradable quiz - + Point value not editable during review - + Visual display on results displays with grey background not as an orange "pending" question -+ Table schema uses default database charset. Fixes an issue with databases that don't support `utf8mb4` charsets. -+ Updated `LLMS_Hasher` class for better compatibility with older versions of PHP - - -v3.16.8 - 2018-02-13 --------------------- - -##### Updates - -+ 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/). -+ 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. -+ Allow filtering when retrieving student answer for a quiz attempt question via `llms_quiz_attempt_question_get_answer` filter - -##### Bug Fixes - -+ 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. -+ 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. -+ Fix analytics widget undefined index warning during admin-ajax calls. Thanks [@Mte90](https://github.com/Mte90)! -+ Fix issue causing `is_search()` to be called incorrectly. Thanks [@Mte90](https://github.com/Mte90)! -+ Fix issue preventing text / html formatting from saving properly for access plan description fields -+ Fix html character encoding issue on reporting widgets causing currency symbols to display as a character code instead of the symbol glyph. - -##### Templates changed - -+ templates/quiz/results-attempt-questions-list.php -+ templates/quiz/results-attempt.php - - -v3.16.7 - 2018-02-08 --------------------- - -+ 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. -+ Added an "Exit" button to the builder sidebar to allow exiting the builder back to the WP Edit Post screen for the current course -+ Added dashboard links to the WP Admin Bar to allow existing the course builder to various areas of the dashboard -+ Added data attribute to progress bars so JS (or CSS) can read the progress of a progress bar. Thanks [@dineshchouhan](https://github.com/dineshchouhan)! -+ Fixed issue causing newly created lessons to lose their assigned quiz -+ Fixed php `max_input_vars` issue causing a 400 Bad Request error when trying to save large courses in the course builder -+ Removed reliance on PHP bcmath functions - - -v3.16.6 - 2018-02-07 --------------------- - -+ Removed reliance on PHP Hashids Library in favor of a simpler solution with no PHP module dependencies -+ Added interfaces to allow customization of quiz url / slug -+ Fixed [audio] shortcodes added to quiz question descriptions -+ Fixed untranslatable strings on frontend of quizzes -+ Fix issue causing certificate notifications to display as empty -+ Fix issue preventing quiz pass/fail notifications from triggering properly for manually graded quizzes -+ Fix undefined index warning on quiz pass/fail notifications - - -v3.16.5 - 2018-02-06 --------------------- - -+ Fix issue preventing manually graded quiz review points from saving properly -+ Improved background updater to ensure scripts don't timeout during upgrades -+ Admin builder JS now minified for increased performance -+ Made frontend quiz and quiz-builder strings output via Javascript translatable - - -v3.16.4 - 2018-02-05 --------------------- - -+ Fix issue causing newly created quizzes to not be properly related to their parent lesson -+ Fix issue preventing quiz time limits from starting unless an attempt limit is also set -+ Fixes a WP Engine issue that prevented the builder from loading due to a blocked dependency - - -v3.16.3 - 2018-02-02 --------------------- - -+ When switching a quiz to "Published" it will now update the parent lesson to ensure it's recorded as having an enabled quiz. -+ 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. -+ 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 -+ Fixed issue causing points percentage calculation tooltip on quiz builder to show the incorrect percentage value -+ Fix issue preventing lessons with no drip settings from being updated on the WP post editor -+ Fix issue causing 500 error on lesson settings metabox for lessons not attached to sections -+ Add a "Quiz Description" field to allow quiz post content to be edited on the quiz builder -+ 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 - - -v3.16.2 - 2018-02-02 --------------------- - -+ Add an update notice to 3.16.0 migration scripts to provide more information about the major update. -+ Removed quiz assignment fields on the lesson metabox to reduce confusion as quizzes are now managed exclusively on the quiz builder. -+ Ensure questions migrated during 3.16 updates retain their initial points value from the quiz. - - -v3.16.1 - 2018-02-01 --------------------- - -+ Ensure quizzes in draft mode are only accessible by those with edit access (instructors, admins, etc...) -+ Restore pre 3.16 actions and filters related to quiz start buttons -+ Remove legacy error message for quiz accessibility issues by site admins -+ Students who cannot access a quiz are redirected to the parent lesson if they attempt to access a quiz directly -+ Fix undefined index warning on wp-login.php related to LifterLMS js assets. Thanks [Mte90](https://github.com/Mte90)! -+ Update checkout error message to provide user with direction when they already have access to a course. Thanks [@andreasblumberg](https://github.com/andreasblumberg)! - - -v3.16.0 - 2018-02-01 --------------------- - -##### Quizzes - -+ New question types: True/False, Picture Choice, and Non-question content -+ Picture & Multiple choice have options for multiple correct answers (checkbox-like questions) -+ You can now create questions with NO POINTS (maybe for surveys?) -+ Upgraded student quiz review interface -+ Upgraded instructor quiz attempt review interface -+ Admins may now leave remarks on questions directly -+ Improved data available related to quizzes and quiz attempts on reporting screens -+ Improved quiz user interface -+ Added a progress bar to the quiz interface -+ Shrunk the quiz timer -+ Added a question # counter on the quiz interface -+ Fixed issue causing randomized questions to get "lost" when navigating back through a quiz attempt -+ Improved error handling on quizzes -+ Overhauled quiz data structure for improved performance and scalability -+ Requires database migration and update: [3.16.0](https://lifterlms.com/docs/lifterlms-database-updates/#3160) - -##### Course Builder Improvements - -+ Quiz-building is now available on the course builder -+ Quiz and Question WordPress editors no longer available. Quizzes and Questions HAVE NOT DISAPPEARED, they've been improved and relocated -+ 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. - -##### Updates - -+ Added space between arrows and "Next" and "Previous" text on pagination lists. Thanks [sujaypawar](https://github.com/sujaypawar)! -+ Updated Quiz post type slug from "llms_quiz" to "quiz". -+ Updated default return of `llms_get_post()` to be `false` rather than a `WP_Post` object when a LifterLMS post cannot be located - -##### Bug Fixes - -+ Fixed a potential database read error related to database store abstract -+ Now passing Post ID as second parameter to the `the_title` filter called on post model getters - - -##### Removed templates - -The 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. - -+ templates/content-single-question-after.php -+ templates/content-single-question-before.php -+ templates/quiz/next-question.php -+ templates/quiz/previous-question.php -+ templates/quiz/question-count.php -+ templates/quiz/quiz-question.php -+ templates/quiz/single-choice.php -+ templates/quiz/single-choice_ajax.php -+ templates/quiz/summary.php -+ templates/quiz/timer.php -+ templates/quiz/wrapper-end.php -+ templates/quiz/wrapper-start.php - -##### Removed Functions - -Various template functions related to quizzes were removed due to the deprecation of their related templates - -+ `lifterlms_template_quiz_timer()` -+ `lifterlms_template_single_next_question()` -+ `lifterlms_template_single_prev_question()` -+ `lifterlms_template_single_single_choice()` -+ `lifterlms_template_single_single_choice_ajax()` -+ `lifterlms_template_single_question_count()` - - -v3.15.1 - 2017-12-05 --------------------- - -+ Ensure course & membership titles with HTML characters are decoded during reporting exports -+ Fix issue causing some courses to display in membership columns on reporting exports - - -v3.15.0 - 2017-12-04 --------------------- - -##### Reporting Updates (and CSV exports!) - -+ Added course-level reporting table (see "Courses" tab of Reporting screen) -+ Updated the interface on reporting screen when reviewing a single student -+ Added reporting exports: students list, courses list, and list of students per course - -##### Bug fixes - -+ Fix error when `[lifterlms_course_continue_button]` shortcode is displayed to logged out or students not enrolled in the chosen course - -##### Minor updates - -+ Tested up to WordPress 4.9.1 -+ Added background data processors to ensure reporting data stays up to date in close to real time -+ Add nocache constants and headers on student dashboard & checkout page to increase compatibility with caching plugins -+ Added filter to student dashboard courses query - - -v3.14.9 - 2017-11-27 --------------------- - -+ Tested up to WordPress 4.9 -+ Fix error during uninstall related to missing file -+ Fix issue with rewinding quiz using "Previous Question" button -+ On final question of a quiz the "Next Lesson" button now says "Complete Quiz" -+ When completing a quiz, the loading message will now say "Grading Quiz" the entire time instead of "Loading Question" and then "Grading Quiz" -+ Fix issue causing the `` element on course builder pages from being partially empty - - -v3.14.8 - 2017-11-06 --------------------- - -+ Lessons can be cloned via the "Clone" action from the lessons post table - -##### Builder Improvements & Fixes - -+ Add "Existing Lesson" functionality can now clone and attach the clone (when adding a lesson currently attached to a course) OR attach orphans -+ Lessons created via Course builder will have their slugs renamed the first time the lesson title is updated via the builder -+ No longer display notices on the course builder -+ Add extra space to the scrollable area on course builder -+ Removed logging and debugging functions from admin builder class -+ JS-generated error messages on the course builder are now translatable - -##### Bug Fixes - -+ Fix: Show all memberships on dashboard - - -v3.14.7 - 2017-10-25 --------------------- - -##### Navigation Menu Items - -+ Add LifterLMS endpoints to your nav menu -+ Add Sign In and Sign Out links which display conditionally based on whether or not the visitor is logged in -+ Checkout the docs at [https://lifterlms.com/docs/lifterlms-navigation-menu-items/](https://lifterlms.com/docs/lifterlms-navigation-menu-items/) - -##### Bug Fixes - -+ Fix SQL query issue with orphaned lesson query on course builder -+ Fix undefined index warning occurring during theme switches -+ Fix issue causing duplicate error messages to display on certain servers - - -v3.14.6 - 2017-10-21 --------------------- - -+ Fix: `<iframes>` are no longer stripped when exporting or duplicating courses (this applies to lessons within the courses as well) -+ Fix: Achievements on student dashboard now output the correct achievement title -+ Fix: Courses on student dashboard ordered by Order attributes will obey settings correctly - - -v3.14.5 - 2017-10-14 --------------------- - -+ Course builder will persist open/collapsed state of sections when they are re-ordered -+ Course builder lessons in a section are draggable after reordering a section - - -v3.14.4 - 2017-10-13 --------------------- - -+ 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. -+ Lessons added to a section will no longer visually disappear when editing a section title on the course builder -+ BuddyPress integration BP template fixes - - -v3.14.3 - 2017-10-12 --------------------- - -+ Fix [lifterlms_my_account] shortcode issue affecting Divi theme users - - -v3.14.2 - 2017-10-11 --------------------- - -+ 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 -+ include the admin notices class when running database update functions - - -v3.14.1 - 2017-10-10 --------------------- - -+ Fix `[lifterlms_my_achievements]` shortcode -+ Fix reference to deprecated core function related to checking the permissions of content restricted to a membership -+ Builder titles will be saved on all field focusout/blur events, not just tab & enter key presses -+ LifterLMS custom meta save metaboxes will not trigger actions during ajax requests -+ Fix issue displaying certificates on admin panel reporting screens - - -v3.14.0 - 2017-10-10 --------------------- - -+ Updated JS for 3.13 course builder to address issues on PHP 5.6 servers with asp_tags enabled -+ Normalized date returns with various dates related to enrollments, achievements, and certificates. These dates now utilize the WP Core `date_format` option. -+ Fixed strict comparison issue related to database query abstract (affected checks for last page & first page on admin reporting screens) -+ Added a new capability `llms_instructor` for admins, lms managers, instructors, and instructor's assistant to easily differentiate "instructors" from "students" -+ Fix `$wpdb->prepare` issue related to notification queries. Fixes WP 4.9-beta issue. - -##### Student Dashboard Updates - -+ Achievements on student dashboard now viewable in popover modal. -+ Achievements tab added to student dashboard -+ Courses, Memberships, Achievements, and Certificates have been updated to have a unified style -+ Courses & Memberships extend the default catalog tiles -+ 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) - -##### Deprecated functions - -+ `LLMS_Student_Dashboard::output_courses_content()` replaced with `lifterlms_template_student_dashboard_my_courses( false )` -+ `LLMS_Student_Dashboard::output_dashboard_content` replaced with `lifterlms_template_student_dashboard_home()` - -##### Template Updates - -+ [achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/achievements/loop.php) -+ [achievements/template.php](https://github.com/gocodebox/lifterlms/blob/master/templates/achievements/template.php) -+ [certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/certificates/loop.php) -+ [certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/certificates/preview.php) -+ [loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop.php) -+ [loop/content.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/content.php) -+ [loop/enroll-date.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/enroll-date.php) -+ [loop/enroll-status.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/enroll-status.php) -+ [loop/pagination.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/pagination.php) -+ [myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php) -+ [myaccount/dashboard.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard.php) -+ [myaccount/header.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/header.php) - -##### Deleted Templates - -+ /myaccount/my-achievements.php -+ /myaccount/my-courses.php -+ /myaccount/my-memberships.php - - -v3.13.1 - 2017-10-04 --------------------- - -+ Fix caching issue preventing quiz pass & fail engagements from triggering. -+ Fix issue causing the "Builder" link to display on the lesson post table screen. -+ Fix issue preventing new courses & memberships from being moved from draft -> published. -+ Fix `wpdb->prepare()` empty placeholder issue related to engagement queries. Fixes warning added in WP 4.9. -+ Add better version numbering to static assets to prevent caching issues during plugin updates - - -v3.13.0 - 2017-10-02 --------------------- - -##### An All New Course Builder - -+ 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 -+ Asynchronous loading: fixes issues where very large courses would drastically slow and possibly even time out the loading of the course edit screen -+ 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 -+ 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 -+ Add new lessons and sections with a click or drag a new lesson or section into the existing course -+ 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! -+ Delete sections and lessons with the click of a button -+ Quick links to view (frontend) and edit (backend) lessons -+ Completely internationalized. Thanks for you patience translators! -+ Want to know more? Check out the [docs](https://lifterlms.com/docs/using-course-builder/). - -##### New User Roles - -+ 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. -+ New Roles: - - + LMS Manager: Do everything in LifterLMS and nothing with plugins, themes, core settings, and so on - + Instructor: Create, update, and delete courses and memberships - + Instructor's Assistant: Edit courses and memberships - -+ More details and a full list of new LifterLMS capabilities are available [here](https://lifterlms.com/docs/roles-and-capabilities/). - -##### Updates & Fixes - -+ Tested up to WordPress 4.8.2 -+ The "Lesson Tree" metabox has been replaced with a simplified version of the lesson tree and a link to the launch the Course Builder. -+ 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. -+ Removed `var_dump()` from bbPress integration restriction check - -##### Uninstall Script - -+ 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/). - -##### Database Update - -+ Adds default Instructor data for all LifterLMS Courses & Memberships based off of the post author of the course or membership -+ [More information](https://lifterlms.com/docs/lifterlms-database-updates/#3130) - -##### Template Updates - -+ [admin/post-types/students.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/students.php) -+ [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php) - -##### Deprecated Functions - -+ 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: - - + `LLMS_AJAX::get_achievements()` - + `LLMS_AJAX::get_all_posts()` - + `LLMS_AJAX::get_associated_lessons()` - + `LLMS_AJAX::get_certificates()` - + `LLMS_AJAX::get_courses()` - + `LLMS_AJAX::get_course_tracks()` - + `LLMS_AJAX::get_emails()` - + `LLMS_AJAX::get_enrolled_students()` - + `LLMS_AJAX::get_enrolled_students_ids()` - + `LLMS_AJAX::get_lesson()` - + `LLMS_AJAX::get_lessons()` - + `LLMS_AJAX::get_lessons_alt()` - + `LLMS_AJAX::get_memberships()` - + `LLMS_AJAX::get_question()` - + `LLMS_AJAX::get_sections()` - + `LLMS_AJAX::get_sections_alt()` - + `LLMS_AJAX::get_students()` - + `LLMS_AJAX::update_syllabus()` - -##### Removed Filters - -+ The following filters have been removed and are no longer in use. - - + `lifterlms_admin_courses_access`: replaced with user capability `edit_courses` - + `lifterlms_admin_membership_access`: replaced with user capability `edit_memberships` - + `lifterlms_admin_reporting_access`: replaced with user capability `manage_lifterlms` - + `lifterlms_admin_settings_access`: replaced with user capability `manage_lifterlms` - + `lifterlms_admin_import_access`: replaced with user capability `manage_lifterlms` - + `lifterlms_admin_system_report_access`: replaced with user capability `manage_lifterlms` - - -v3.12.2 - 2017-09-18 --------------------- - -##### Bug fixes - -+ Fix issue with LifterLMS bbPress integration preventing course-restricted topics from being accessible by enrolled students -+ Fix an issue preventing students expired from courses via access expiration settings from being manually re-enrolled by admins - -##### Deprecations - -+ `LLMS_Student` class function `has_access` is scheduled for deprecation in next major release. Developers should switch to `LLMS_Student->is_enrolled()` - - -v3.12.1 - 2017-08-25 --------------------- - -+ Prevent duplicate loading of repeater metabox fields -+ Fix undefined warning related to quiz completion -+ Ensure that the bbPress course forums shortcode & widget properly cascade up when used on a lesson or quiz - - -v3.12.0 - 2017-08-17 --------------------- - -+ New quiz feature: randomize the order of quiz questions each attempt! Props to [Larry Groebe](https://github.com/larrygroebe) -+ Fixed logic error related to access checks when bubbling from quiz->lesson->course -+ Fixed JS loader check for tinyMCE editors in repeater fields -+ Fixed CSS issue related to tinyMCE editors in repeater fields -+ Fixed issue causing tinyMCE editors in repeater fields to stop working after reordering rows -+ LifterLMS alert box notices are now cleared during shutdown instead of immediately after rendering. Fixes some plugin compatibility issues. -+ Fix reference to invalid meta key on order notes admin screen. -+ Record order note when orders with a defined length complete -+ When a payment is scheduled for an order with a defined length, calculate end date if no end date is saved -+ Minor updates to the `LLMS_Abstract_Integration` class -+ Fix undefined reference error on 404 pages resulting from the preview manager. - -##### bbPress Integration Updates - -+ Add "Private" Course Forums which allows forums to be made available only to students enrolled in the associated course -+ Adds a shortcode and widget for outputting a list of forums associated with a course -+ Adds the ability to restrict the page set as the bbPress forum index (via bbPress settings) to be restricted to LifterLMS memberships -+ Adds engagement triggers to allow engagements to be fired when a student posts a reply or creates a new topic -+ Improves integration membership restriction check performance -+ Migrated to the `LLMS_Abstract_Integration` class. Visually changes the settings display but has no other impact -+ [More information](https://lifterlms.com/docs/lifterlms-and-bbpress/) - -##### BuddyPress Integration Updates - -+ Add the ability to restrict activity, group, and member directory pages to LifterLMS memberships. -+ Migrated to the `LLMS_Abstract_Integration` class. Visually changes the settings display but has no other impact -+ [More information](https://lifterlms.com/docs/lifterlms-and-bbpress/) - -##### Database update - -+ 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. -+ migrate bbPress and BuddyPress options to `LLMS_Abstract_Integration` naming convention -+ [More information](https://lifterlms.com/docs/lifterlms-database-updates/#3120) - -##### Admin Post Table Upgrades - -+ Lessons - + Fix section titles which formerly were a dead link. Now they're just text - + Add filtering the table by associated course -+ Quizzes - + Display associated course and lesson columns with links - + Add filtering by associated course and/or lesson -+ Quiz Questions - + Display associated Quizzes with links - + Add filtering by associated quiz - -##### Template Updates - -+ [admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php) - - -v3.11.2 - 2017-08-14 --------------------- - -+ Tested up to WP Core 3.8.1 - -##### System Status and Reporting updates - -+ System Report renamed to "Status" -+ Added information of template overrides to the system report -+ Added "Get Help" button linking to LifterLMS Ticketing submission page -+ Added "Logs" tab which allows for easy viewing & management of LifterLMS logs -+ Added "Tools and Utilities" tab and moved tools from the General Settings screen to this tab -+ Improved Session Reset tool - - -v3.11.1 - 2017-08-03 --------------------- - -+ New shortcode: `[lifterlms_course_continue_button]`. See [shortcode docs](https://lifterlms.com/docs/shortcodes/#lifterlms_course_continue_button) for more information. -+ New shortcode: `[lifterlms_lesson_mark_complete]`. See [shortcode docs](https://lifterlms.com/docs/shortcodes/#lifterlms_lesson_mark_complete) for more information. -+ Added filter `llms_product_pricing_table_enrollment_status` to allow forceful display of course/membership pricing tables regardless of user enrollment status. -+ Fix course author shortcode to allow usage outside of a course via the `course_id` parameter. - -##### Template Updates - -+ [product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/pricing-table.php) -+ [product/course/progress.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/course/progress.php) - - -v3.11.0 - 2017-07-31 --------------------- - -+ New engagement trigger "Student purchases access plan" allows engagements to be triggered from a specific access plan! -+ Minor performance improvements to notification-related database queries -+ Fix issue causing payment gateways to always use test mode links from Orders on the admin panel -+ Added default email notification merge code for outputting an HTML divider -+ Added new actions to Dashboard template to allow adding custom content to course tiles on the dashboard - -##### Template Updates - -+ [myaccount/my-courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-courses.php) - - -v3.10.2 - 2017-07-14 --------------------- - -+ Fix fatal error related to purchase receipts for trashed or deleted orders -+ l10n "Reviews" tab title on course settings -+ Remove commented out sample preheader text from email header template which was displaying in some email clients. - -##### Template Updates - -+ [emails/header.php](https://github.com/gocodebox/lifterlms/blob/master/templates/emails/header.php) - - -v3.10.1 - 2017-07-12 --------------------- - -##### Bugfixes - -+ Prevent errors related to attempting to display notification data related to deleted students -+ Fix errors related to displaying notifications for deleted post (courses, sections, lessons, quizzes, etc...) -+ Fix error causing email notifications being sent after related user has been deleted -+ Fix typo preventing `llms_form_field()` from outputting textareas - -##### Updates - -+ 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). -+ Add new API for querying students via AJAX select2 elements -+ Select2 Post Query elements can now query multiple post types simultaneously -+ Seletc2 Post Query elements can now support `<optgroup>` - -###### i18n - -+ Course option metabox for reviews is not translatable - - -v3.10.0 - 2017-07-05 --------------------- - -##### Recurring Order Management (for Admins) - -+ Admins can now edit various pieces of data related to a recurring order from the order screen on the admin panel - + Allow editing of the Next Payment Date - + Allow editing of the Trial End Date (when a trial is active for the order) - + Edit Payment Gateway and related gateway fields (Customer ID, Source ID, and Subscription ID) -+ 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! - -##### Recurring Order Management (for Students) - -+ Students can now switch the payment method (source) for their recurring subscriptions from the student dashboard -+ Students can now cancel their recurring orders to prevent future payments on recurring orders -+ 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! - -##### Automatic Payment Retries (for supporting gateways) - -+ 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/). -+ 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! - -##### Manual Payment Gateway Enhancements - -+ 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/). - -##### Updates and Fixes - -+ 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. -+ Fixed spelling error related to quizzes - -##### Templates changed - -**NEW** - -+ [checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-switch-source.php) -+ [myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order-transactions.php) - -**UPDATED** - -+ [admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php) -+ [myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-orders.php) -+ [myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/navigation.php) -+ [myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php) -+ [quiz/summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/summary.php) - - -v3.9.5 - 2017-06-13 -------------------- - -+ Increased css z-index of basic notifications to prevent issues with themes that have high z-index on menus and other elements -+ Increased the frequency of basic notification heartbeat check from 10 to 20 seconds -+ Added filter to allow for customization of the notifications heartbeat interval, example [here](https://lifterlms.com/docs/lifterlms-filters/#llms_notifications_settings). -+ Fixed error related to password reset when the "Disable Usernames" account setting is disabled - - -v3.9.4 - 2017-06-12 -------------------- - -+ Fix hardcoded db reference to `wp_posts` table - - -v3.9.3 - 2017-06-09 -------------------- - -+ Fix typo in notifications query - - -v3.9.2 - 2017-06-07 -------------------- - -+ Tested up to WordPress 4.8 -+ Fixed issue with merge codes on WP Editors for notifications, emails, etc... -+ 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. -+ Only display quiz time limit meta information when a time limit exists -+ Fix display of quiz question order (question x of x) -+ Improved logic powering quiz attempt grading for increased consistency, especially with regards to floats and rounding - -##### Templates Changed - -+ [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php) -+ [quiz/question-count.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/question-count.php) - - -v3.9.1 - 2017-06-02 -------------------- - -+ Fix engagement triggers with relation to quizzes to properly receive 3.9 api updates -+ Fix quiz attempt counting issue resulting in the total attempts by a student always being one more than the actual value -+ Fix membership access plan restrictions tooltip - - -v3.9.0 - 2017-06-02 -------------------- - -##### Quizzes - -+ All new quiz results interface for students - + Donut charts are now animated - + Donuts will be green for passing attempt and red for failing - + Students can now review previous quiz attempts and summaries - + Removed the juxtaposition of the current and best attempts to reduce confusion on the interface - + Improved the consistency of the quiz meta information markup - + Adjusted various pieces of language for an improved student experience -+ Improvements to the quiz taking experience - + 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 - + Better error handling and management should issues arise during a quiz - + Better unload & beforeunload JS management to warn students when they attempt to leave a quiz in progress -+ Improved quiz data handling and management - + Improved API calls and handlers related to taking quizzes for increased performance and consistency - + quiz data can now be programmatically queried via consistent apis and data classes, see `LLMS_Student->quizzes()` and `LLMS_Quiz_Attempt` -+ 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. -+ Deprecated `LLMS_Quiz::start_quiz()`, `LLMS_Quiz::answer_question()`, and, `LLMS_Quiz::complete_quiz()` - + Ajax handler functions of the same names should be used instead. - + To programmatically "take" quizzes use related functions of similar names from the `LLMS_Quiz_Attempt` class - -##### Templates changed - -+ New - + [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php) - -+ Updated - + [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php) - + [content-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/content-certificate.php) - + [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php) - + [myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php) - + [quiz/next-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/next-question.php) - + [quiz/previous-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/previous-question.php) - + [quiz/question-count.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/question-count.php) - + [quiz/quiz-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-question.php) - + [quiz/quiz-wrapper-end.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-wrapper-end.php) - + [quiz/quiz-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-wrapper-start.php) - + [quiz/results.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results.php) - + [quiz/return-to-lesson.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/return-to-lesson.php) - + [quiz/single-choice_ajax.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/single-choice_ajax.php) - + [quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/start-button.php) - + [quiz/summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/summary.php) - -+ Removed - + quiz/attempts.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php) - + quiz/passing-percent.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php) - + quiz/time-limit.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php) - -##### Fixes - -+ Student Dashboard notifications page will not display pagination links unless there's results to page through -+ Student Dashboard notifications page will now display a message when no notifications are found -+ Certificate previewing now takes into consideration the preview setting roles to allow admins (or other roles) to preview certificates -+ Made student name self fallback (you) i18n friendly - - -v3.8.1 - 2017-05-21 -------------------- - -+ Fix merge code issue related to course title on quiz notifications - - -v3.8.0 - 2017-05-20 -------------------- - -+ Automatic email and basic (on-screen) notifications for various events within LifterLMS - + All notifications can be customized - + Email notifications can be optionally sent to custom email address, course authors, and more -+ Students will automatically receive email receipts when making purchases and when recurring access plans rebill -+ Hidden Access Plans -+ Add a "Purchase Link" view button to access plans so admins can quickly grab the direct URL to an access plan -+ Notifications history screen on Student Dashboard to review past notifications that have been received -+ Updated LLMS_Email class and functionality -+ Email templates have been completely rewritten and styled -+ Updated and rewritten password reset flow -+ Earned certificates are only accessible by the student who earned the certificate -+ Added the functionality for image upload via options & settings api -+ 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. -+ Fixed filter on engagements settings page -+ Minor adjustments to language and settings order on Engagements settings screen for email settings -+ Email Header Image field is now an upload field as opposed to a "paste a url here" setting -+ Phone number recorded to order and displayed on order for admin panel during purchases -+ Order details now display full country name as opposed to the country code -+ Fix installation script to ensure admin can preview by default - - -v3.7.7 - 2017-05-16 -------------------- - -+ Updated a few strings on the admin panel to be translatable -+ Fix PHP warning output during plugin activation -+ Fix reporting issue related to outputting quiz question answers where the correct answer is the first available answer -+ Fix PHP 7.1 issue on the checkout screen -+ Removed some unnecessary files from vendor libraries - - -v3.7.6 - 2017-05-05 -------------------- - -+ New translations for new categories on Add-ons screen -+ Update to general settings which utilizes featured items from the general settings screen -+ Update readme & related meta files -+ Removed advert image files - - -v3.7.5 - 2017-05-02 -------------------- - -+ Upgrade WP Session Manager to latest version -+ 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) - - -v3.7.4 - 2017-04-26 -------------------- - -+ When cloned site detected automatically disable recurring_payments feature & trigger an action 3rd parties can hook into for custom 3rd party features -+ Add better JS dependency management to prevent issues where assets loaded in the wrong order -+ Fix issue where dismiss icon on LifterLMS admin notices was positioned poorly on non-LifterLMS admin screens -+ Fix issue preventing edit account form submission on student dashboard when password strength meter is disabled - - -v3.7.3 - 2017-04-21 -------------------- - -+ Fixed issues where Course Track checks were not functioning properly with relation to prerequisite associations -+ `LLMS_Generator` can now be used to generate course(s) from a raw array of course data using the SingleCourseGenerator and BulkCourseGenerator -+ `LLMS_Generator` default post status can be set at runtime using `set_default_post_status()` -+ Fixed an issue causing JS errors on the `wp-login.php` screen -+ Tested up to WordPress 4.7.4 - -### Template Updates - -+ `course/prerequisites.php` - Prerequisite checks check for 'course_track' rather than 'track' - - -v3.7.2 - 2017-04-17 -------------------- - -+ Resolved a JS errors on admin panel resulting from overly strict asset loading added in 3.7.0 - - -v3.7.1 - 2017-04-14 -------------------- - -+ Fix php notice when no roles are selected for preview management feature - - -v3.7.0 - 2017-04-13 -------------------- - -**Preview Management** - -+ All new view management for users to make editing content easier for course builders -+ Admins may customize the roles of users who can access view management -+ Qualifying users can view content as an enrolled student or a non-enrolled visitor -+ Default view allows users to bypass all restrictions (drip, membership, enrollment, and so on) for easy course navigation and management -+ 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! - -**Improvements** - -+ Edit Account Screen now utilizes updated APIs for better customization management -+ Improve intelligence of enqueued admin js & css files - -**Fixes** - -+ Fixed coupon calculation issue related to currencies using commas as the decimal separator -+ Properly display track related information when reviewing engagements on the admin panel -+ fixed issue preventing course tracks from being recorded as completed - - -v3.6.2 - 2017-04-10 -------------------- - -+ Fix issue preventing export of vouchers via email -+ added action `after_llms_mark_complete` to allow custom actions to happen after a course, lesson, etc... is marked complete - - -v3.6.1 - 2017-03-28 -------------------- - -+ Fix issue related to taking a quiz for the first time when no quiz data is available for a user -+ Fix issue when course outline shortcode is displayed on non LifterLMS post types - - -v3.6.0 - 2017-03-27 -------------------- - -+ 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/). -+ 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. -+ Added options (and filters) to allow customization of the order of courses displayed on the Student Dashboard - + Existing behavior (ordered by enrollment date, most recent to least recent) will be preserved - + New installations will default (by popular demand) to Order (Low to High) which will obey the "Order" settings of courses - + Customize or update the order for your site by visiting LifterLMS -> Settings -> Accounts and changing the setting for "Courses Sorting" under "Account Dashboard" -+ New Shortcodes: - + `[lifterlms_course_author]` - Display the Course Author's name, avatar, and (optionally) biography. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_author) - + `[lifterlms_course_continue]` - Display a progress bar and continue button for enrolled students only. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_continue) - + `[lifterlms_course_meta_info]` - Display all meta information for a course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_meta_info) - + `[lifterlms_course_prerequisites]` - Display a notice describing unfulfilled prerequisites for a course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_prerequisites) - + `[lifterlms_course_reviews]` - Display reviews and review form for a LifterLMS Course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_reviews) - + `[lifterlms_course_syllabus]` - Display the course syllabus. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_syllabus) -+ "Back" & "Next" pagination links on Student Dashboard View Courses are now buttons instead of text links -+ Fixed an issue preventing pagination links from displaying on the "View Courses" page of the student dashboard when the endpoint slug was customized -+ Course and Membership taxonomy archive pages will now properly match the heights of tiles -+ Fixed typo in `lifterlms_get_enrollment_status_name` filter -+ Fixed typo in `lifterlms_get_order_status_name` filter -+ Reduced complexity and redundancy of `llms_get_enrolled_students()` - - -v3.5.3 - 2017-03-21 -------------------- - -+ Ensure that access plan subscription schedule details are fully translatable -+ Ensure "Services" title on admin add-ons screen can be translated -+ Fix "View All My Courses" link on Student Dashboard to obey endpoint slug customizations -+ Membership restriction checks only run on singular posts (not on archives) -+ Ensure `[lifterlms_course_outline]` and Course Syllabus widget can be used on Quizzes. -+ Fix reporting widgets for course & lesson completions to report the correct completion types only - - -v3.5.2 - 2017-03-16 -------------------- - -+ Fix course outline shortcode when used on a lesson -+ Fix custom html form fields produced by `llms_form_field()` - - -v3.5.1 - 2017-03-15 -------------------- - -+ 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 -+ Updated course outline shortcode / course syllabus widget to utilize new APIs -+ 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 -+ Fix issue preventing course auto advance on lesson completion -+ Shortcodes added within `[lifterlms_hide_content]` will now be processed - - -v3.5.0 - 2017-03-13 -------------------- - -+ 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. -+ Course and Membership catalog per page settings will now only accept numbers -+ "Catalogs" settings tab has been split into "Course" and "Membership" settings -+ Settings added via filter `lifterlms_catalogs_settings` will be added to the "Course" settings tab and deprecated in the next major release -+ 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. -+ Tweaked size of LifterLMS admin tab menu items -+ Pass API Mode Context to links generated by LifterLMS payment gateways -+ Fixed typo on general settings screen -+ Moved LifterLMS Add-on Banners from General Settings to an Add-Ons menu -+ 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 -+ Updated action scheduler lib to latest version. Minor changes, fixes compatibility with WooMemberships. -+ Recent activity stats widgets on general settings screen updated to be more reliable and accurate (and performant!) -+ Added 3 new widgets to enrollments reporting tab: courses completed, lessons completed, and user registrations - - -v3.4.8 - 2017-03-07 -------------------- - -+ Tested to WordPress Version 4.7.3 -+ Fixed undefined index notice on admin panel -+ Added a real description to new `_nx()` functions -+ Access plan trial periods now allow proper translations - - -v3.4.7 - 2017-03-03 -------------------- - -+ Ensure run when the `lifterlms_db_version` option doesn't exist in the database - - -v3.4.6 - 2017-03-03 -------------------- - -+ Fixed a text domain typo preventing translation of "Correct Answer" on quiz results screen -+ Ensure access plan "periods" are translatable -+ Now using `date_i18n()` for certificate dates so that dates are properly localized -+ Load plugin textdomain during `init` rather than `plugins_loaded` - - -v3.4.5 - 2017-02-23 -------------------- - -+ Ensure free access plans are available to logged out users - - -v3.4.4 - 2017-02-22 -------------------- - -+ Added a popup to warn students when leaving a quiz they've already started -+ Enable removal of student quiz attempts by admins from student reporting screens -+ Fix an undefined error on quiz reporting screens for incomplete quizzes -+ Display incomplete (abandoned) quizzes as incomplete (instead of as still running) on the quiz reporting screen -+ Prevent logged in users from bypassing membership restrictions for free members-only access plans - - -v3.4.3 - 2017-02-20 -------------------- - -+ 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 -+ Ensure that the correct ajax url is used for quizzes, resolves issue for sites utilizing `FORCE_SSL_ADMIN` -+ Refactored database background update scripts for increased reliability & performance -+ 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. -+ WIP: refactoring shortcodes to a more sane set of functions and classes - - -v3.4.2 - 2017-02-14 -------------------- - -+ Backwards compatible css for tooltips - - -v3.4.1 - 2017-02-14 -------------------- - -+ Password strength meter now functions correctly when using the [lifterlms_registration] shortcode -+ Ensure open registration with required voucher prevents registration with invalid vouchers -+ Lesson completion via quiz completion only recorded the first time the quiz is completed -+ Fix issue preventing membership catalog from obeying the catalog's ordering settings -+ Prevent duplicate engagements from being triggered -+ Admin tables can display percentages as a progress bar! -+ Students reporting table displays overall progress as a progress bar -+ Refactored frontend assets class to allow better management of inline scripts - - -v3.4.0 - 2017-02-10 -------------------- - -+ 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/) -+ Upgraded Student Management Table for courses and memberships: - + Allow searching students by name / email - + Allow filtering of students by current status - + Allow sorting of students by name, user id, status, and enrollment updated date - + Added student's grade to the table (courses only) - + Table pagination allows skipping to the first and last pages - + Student names link to full student reporting screen - + Student IDs added to the table. ID links to the WP User Edit screen which was previously accessible by clicking the student's name - + Utilizing improved database queries for displaying data on the table -+ 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) -+ Students reporting table pagination can now jump to first and last page -+ Students reporting table pagination now displays current page and total number of pages -+ 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 -+ `LLMS_Admin_Table` abstract now supports filtering and jump to first and last page pagination options -+ `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. -+ Ensure `LLMS_Course::has_prerequisite( 'course' )` & `LLMS_Course::has_prerequisite( 'track' )` always return booleans -+ Made a small performance tweak for courses without audio / video embeds -+ Fix coupon expiration dates check to be more i18n friendly -+ Update `LLMS_Coupon` class to utilize 3.3.0 class property enhancements -+ added `llms_current_time`, a pluggable wrapper for `current_time()` to enable easier unit testing of date-related functions -+ Shortcodes within course restriction messages are now handled properly to output their intend content rather than the raw shortcode -+ Ensure the Page Attributes area is available on lessons so WordPress 4.7 custom post type page templates can be utilized - - -v3.3.1 - 2017-01-31 -------------------- - -+ Tested up to WordPress core 4.7.2 -+ Added new engagement triggers for Quiz completion, quiz failure, and quiz passed. -+ Refactored Lesson Completion for sanity -+ 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. -+ Class function `LLMS_Lesson::mark_complete()` has been staged for deprecation. It will still function but developers should update code to use above function. -+ LifterLMS background updaters will now display a progress report on the admin panel to add some transparency to how the update is doing. -+ Added `author` support to `llms_membership` post type -+ Added a way to remove all LifterLMS-generated data during plugin uninstallation. -+ `llms_get_post()` will now work with any LifterLMS Post Model post types -+ Removed references to `LLMS_Activate` class which was removed back in 2.0. -+ Changed include method to session related classes for better handling via phpunit -+ Refactored some of the `LLMS_Install` class for reliability and test coverage - + Changed order of table and option creation during installation. Prevents a database warning from being thrown during installation. - + Added function for retrieving default difficulty categories added during installation - + Added function for removing default categories added during installation -+ `llms_are_terms_and_conditions_required()` ensure the page id used in this function is an absint -+ Removed redundant function `LLMS_Lesson::single_mark_complete_text()` -+ Add css classes for buttons to be auto-width rather than the width of their containers -+ Fix ID of engagement email class. Allows some filters and actions to actually be used. -+ Properly display quiz failures as failures on the quiz results screen -+ `loop/feature-image.php` now works for unsupported PHP 5.5 and down -+ Fix issue with modifying section titles from within the course builder -+ Fix undefined warning resulting from admin notice "flash" being undefined on pre-existing saved notices -+ Updated template at `templates/course/complete-lesson-link.php` to include a few new CSS classes and utilize `llms_form_field()` to standardize buttons - - -v3.3.0 - 2017-01-23 -------------------- - -+ New course option allows displaying the video embed in place of the featured image on course tiles displayed on the course catalog screen -+ Courses can now be exported individually or in bulk. Export of a course includes all course content, sections, lessons, and quizzes. -+ Courses can now be duplicated. Duplication duplicates all course content, sections, lessons, and quizzes. -+ Upon completion of the Setup Wizard a sample course can be automatically installed. -+ 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) -+ Update to `LLMS_Post_Model` to allow easier programmatic definition and handling of extending class properties -+ classes extending `LLMS_Post_Model` can now be serialized to json and converted to arrays programmatically -+ new function `llms_get_post()` allows easier instantiation of an `LLMS_Post_Model` instance -+ Added LifterLMS Database Version to the system report - - -v3.2.7 - 2017-01-16 -------------------- - -+ Fix float conversion of large numbers with relation to coupon price adjustments - - -v3.2.6 - 2017-01-16 -------------------- - -+ Tested up to WordPress Core 4.7.1 -+ Fix the display of track-related engagements on the engagement admin screen -+ Fix float conversion of large numbers with relation to prices - - -v3.2.5 - 2017-01-10 -------------------- - -+ 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. -+ 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. -+ ensure every return from `llms_page_restricted` is filtered. Thanks to @matthalliday -+ Ensure purchase page can only load for valid access plans -+ Course / Membership taxonomy archives now obey orders defined by their respective catalog settings -+ Fix language of automatic validation error message for numeric field types -+ Fix translation function error causing course syllabus to display incorrect "x of x" text -+ Added correct text domain to an i18n string displayed on the checkout confirmation screen, thanks @ymashev -+ Ensure search result pages are viewable by members and non members regardless of result membership restrictions (unless site is restricted to sitewide membership) - - -v3.2.4 - 2017-01-03 -------------------- - -+ 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 -+ 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 -+ "Quiz Results" title is now translatable -+ Removed deprecated JS file "llms-metabox-data.js" which controlled UI/X for 2.x subscription data on courses and memberships -+ 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 -+ Fixed a redirect loop encountered if direct access to a lesson with an incomplete prerequisite was attempted - - -v3.2.3 - 2016-12-29 -------------------- - -+ Progress and Grade are now sortable columns on the student reporting table -+ Make enrollment statuses translatable for courses and memberships on the Student Dashboard -+ "Sign Out" text on student dashboard is now translatable, thanks @yumashev -+ Fixed prerequisite lesson display on lesson post tables -+ Ensure post archive (blog) is visible regardless of post membership restrictions -+ Moved lesson post table management functions to their own class -+ Unused section post table management functions removed - - -v3.2.2 - 2016-12-21 -------------------- - -+ Adds filter `llms_student_dashboard_login_redirect` allowing customization of the redirect upon login via the Student Dashboard -+ Adds a shortcode parameter, `login_redirect` to `[lifterlms_my_account]` allowing customization of the redirect upon login via the Student Dashboard -+ 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 -+ Fixes a compatibility issue with the OptimizePress live editor -+ Adds a text domain to a translation function where none was present, rendering the string untranslatable - - -v3.2.1 - 2016-12-14 -------------------- - -+ Fix operator position on `is_complete` check - - -v3.2.0 - 2016-12-13 -------------------- - -##### LifterLMS Reporting Beta - -+ Students overview displays broad information about your students in a searchable and sortable table -+ Review data about individual students including: - + Membership enrollments and statuses - + Course enrollments, status, and progress - + Quiz attempts and and their submitted answers - + Earned achievements and certificates -+ Sales and Enrollments analytics are now found under the "Reporting" screen -+ Feedback on the beta? Let us know at [https://lifterlms.com/docs/lifterlms-reporting-beta/](https://lifterlms.com/docs/lifterlms-reporting-beta/) - -##### Other Updates & Fixes - -+ Lesson completion checks now look for at least one record of the completed lesson as opposed to looking for exactly one -+ Fix positioning of teacher avatar on course/membership tiles -+ Remove explicit color definition from Student Dashboard navigation links for greater theme compatibility - - -v3.1.7 - 2016-12-06 -------------------- - -+ Added support for WordPress Twenty Seventeen theme -+ Improved the messaging and functions related to LifterLMS Sidebar support -+ Add alternate language for a quiz requiring 100% grade to pass -+ Added CSS class `.llms-button-primaray` to lesson "Mark as Complete" buttons -+ Add box-sizing css rule to LifterLMS form field elements. Fixes layout issues on themes that don't border-box everything. -+ 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 -+ Fix next payment date error when viewing a cancelled recurring order on the student dashboard -+ Recurring payments now scheduled based on UTC time in accordance with the action scheduler which executes based on UTC rather than site timezone -+ Add existing lesson to course modal now relies on async search. Improves performance and prevents timeouts on sites with a 500+ lessons -+ Removed 2.x -> 3.x update notification message -+ Fix an issue with comment counting on PHP7 -+ Updated action scheduler library to latest version - - -v3.1.6 - 2016-11-11 -------------------- - -+ Handle empty responses on analytics more responsibly -+ Fix typo preventing completed orders from displaying in analytics when using course / membership filters -+ Quiz builder now leverages llmsSelect2 rather than select2 directly. Resolves a number of theme and plugin compatibility issues. -+ Prevent bullets and weird margins on LifterLMS notices with slightly more specific CSS -+ Login error messages will now display regardless of whether or not open registration is enabled -+ Attempts to access quizzes are redirected or error messages are output when student is not enrolled. - - -v3.1.5 - 2016-11-10 -------------------- - -+ Fix Month display on Analytics Screen - - -v3.1.4 - 2016-11-10 -------------------- - -+ Progress bars are slightly more intelligent to prevent a widowed "%" on themes with larger base font sizes -+ LifterLMS Merge code button only displays where it's supposed to now -+ Fix issue where users removed from a membership were not properly removed from courses they were auto-enrolled into because of that membership -+ Fix analytics screen JS parsing error - - -v3.1.3 - 2016-11-04 -------------------- - -+ Added new action hooks to the course syllabus widget/shortcode template -+ Added a small text link on the student dashboard which links to the full courses list of the dashboard -+ Display order revenue for legacy orders instead of 0 -+ Make the Order History table on the Student Dashboard responsive -+ Only display _published_ courses on the student dashboard -+ Fixes a conflict with WP Seo Premium's redirect manager which was creating access plan redirects -+ Reenable course review options on the admin panel -+ Updates review output method so reviews are now output via a removeable action - - -v3.1.2 - 2016-10-31 -------------------- - -+ 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 -+ Fix typo related to the line-height of LifterLMS order notes on the admin panel. Thanks [@edent](https://github.com/edent)! - - -v3.1.1 - 2016-10-28 -------------------- - -+ Shortcode `[lifterlms_hide_content]` has some new functionality. See [documentation](https://lifterlms.com/docs/shortcodes/#lifterlms_hide_content) for usage and more information! -+ Fix logic when determining if terms and condition checkboxes should be displayed on checkout & open registration. -+ Define a placeholder on the Terms & Conditions page selection so it can be removed -+ Explicitly declare `LLMS_Lesson` on lesson audio/video embed templates instead of relying the global `$lesson`. Some environments appear to be losing the global. -+ Removed unused lesson template "full-description" - - -v3.1.0 - 2016-10-27 -------------------- - -+ New engagement triggers available to allow engagements to be fired when a student enrolls into a course or membership! -+ Add custom email addresses for to, cc, and bcc when sending email engagements -+ New Merge Code button for easy merging of custom merge codes when creating emails -+ Added post table data for LifterLMS Engagements -+ Added new filter `llms_email_engagement_date_format` which allows customization of the format of the `{current_date}` merge code available in LifterLMS Emails -+ Added explicit max width declaration to images within LLMS Catalogs to prevent image overflow. Fixes some theme compatibility issues. -+ Optimize course and lesson audio video templates for faster loads -+ Fix course & lesson video to load videos instead of duplicating audio embeds -+ 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. -+ Fix LLMS Engagement Email merge codes to work in subject line - - -v3.0.4 - 2016-10-20 -------------------- - -+ 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) -+ Added internal function `LLMS_Student->get_name()` -+ Three basic course difficulties will be automatically created on installation and upgrades -+ Updated course difficulty save methods to rely only on the taxonomy rather than the taxonomy and postmeta table -+ Updated admin settings screens to only flush rewrite rules on screens where it is necessary to update rewrites -+ Fix issue with customization of LifterLMS account endpoint URLs -+ 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 -+ Fix an undefined index warning on courses / memberships when updating post data -+ Remove confusing and invalid warning message from Membership post screen on admin panel - - -v3.0.3 - 2016-10-17 -------------------- - -+ 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 -+ Fix logic so that only free lessons are marked as free lessons post 3.0 upgrade -+ Fix incorrect display of the "restricted" and "non-restricted" content areas for memberships -+ Fix undefined index warning output by membership metaboxes -+ Fix dead like under "Force SSL" checkout setting -+ Course & Membership tiles output by course or membership shortcodes now automatically match column heights like the default catalogs do. -+ Correctly register students as the "Student" Role -+ Database Upgrade script converts users with the role "studnet" to "student" - - -v3.0.2 - 2016-10-14 -------------------- - -+ Added action `lifterlms_before_student_dashboard_tab` -+ Added action `lifterlms_after_student_dashboard_greeting` -+ Added action `lifterlms_after_student_dashboard_tab` -+ Added action `lifterlms_sd_before_membership` -+ Added action `lifterlms_sd_after_membership` -+ Fix membership shortcode -+ Fix issue that prevented "Student Dashboard" from rendering if the page was set as the child of another page -+ Fix undefined function error in admin notices -+ Fix nonce errors resulting from admin notice html being served from the database rather than being dynamically generated -+ Fix db upgrade script which was enabling course time period for restrictions for all courses regardless of their pre 3.0 restriction settings -+ Fix db upgrade script that was causing empty sale dates to show start of unix epoch b/c they were empty strings -+ Fix Javascript parse error preventing section & lesson editing from within the course outline on the admin panel -+ Fix lesson icons from highlighting lesson settings like drip delay & quiz association -+ Updated course outline color scheme to match the 3.0 admin color scheme overhaul -+ `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. -+ Handle enrollment status of legacy orders based on enrollment rather than enrollment AND order status - - -v3.0.1 - 2016-10-13 -------------------- - -+ Properly prefix `llms_is_ajax()` to prevent 500 errors when leaving HTTPS forced checkout screen -+ Fix student unenrollment from memberships which was leaving a trace of enrollment in the user_meta table -+ Update student dashboard nav list items to have more specific no styles to prevent "double discs" on various themes -+ Return course progress bar and "continue" button which was accidentally removed -+ Added core support for "Divi" theme sidebars - - -v3.0.0 - 2016-10-10 -------------------- - -**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** - -**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.** - -**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.** - -+ New shortcodes to be documented later, checkout "includes/class.llms.shortcodes.php" if you're feeling anxious -+ All kinds of CSS changes to make LifterLMS, in general, be a little less old looking -+ Added a number of CSS classes to various areas in the Checkout template at "templates/checkout/form-checkout.php" -+ Added a "Cancel" button that allows you to hide the coupon form if the user decides not to add a coupon -+ 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. -+ Refactored JavaScript related to LifterLMS Checkout. Improvements are minimal (if any) but the file is now smaller and more readable! Yay code stuff. -+ Fixed some redundant text on single payment confirmation screen. ("Single payment of single payment of") -+ Added a link to memberships listed under "My Memberships" on the LifterLMS Account Screen -+ 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. -+ Fixed undefined variable notice generated by Sections without any lessons inside of them -+ renamed function `add_query_var_product_id()` to `llms_add_query_var_product_id()` -+ added a class for interacting with a course TRACK, instantiated by a track term or term_id (`LLMS_Track`) -+ password strength meter and related settings / options via utilizing WordPress password strength functions available -+ cleaned up the lesson locked tooltips to be a bit more sane and also utilized in course navigation on individual lessons. -+ Updated admin menus for LifterLMS content to be more sane and organized and intuitive and so on and so forth - -##### Payment Gateways - -**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** - -+ Payment gateways powered by a new abstract gateway class -+ PayPal has been removed from LifterLMS and is available as premium extension - -##### Frontend Notices - -+ LifterLMS "Notices" have been rewritten, slightly. -+ Most templates have been updated -+ associated CSS has been updated -+ Some sanity has been added to the related functions - -##### Post "Model" Concept / Overhaul - -Updated classes for programmatically accessing all sorts of data related to custom post types registered by LifterLMS. - -These post types currently include: - -+ Access Plans -- a non-public post type associated with courses and memberships which store payment related information -+ Coupons (replaces includes/class.llms.coupon.php) -+ Courses (replaces includes/class.llms.course.php) -+ Lessons (replaces includes/class.llms.lesson.php) -+ Memberships -+ Orders (replaces includes/class.llms.order.php -+ Products -- can be instantiated from courses or memberships (replaces includes/class.llms.product.php) -+ Transaction -- a non-public post type associated with orders which store completed/attempted transaction data - -##### Improved admin metabox methods (and related) - -+ Updated custom LifterLMS Admin Metaboxes to have a more sane programmatic interface. This affects nearly all admin metabox classes in the plugin. -+ 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" - -##### Coupons - -+ New class `LLMS_Coupon` allows for easy getting & setting of coupon data. -+ Updated coupon post table to include relevant coupon information for all coupons at a glance -+ Refactored admin panel coupon metabox generation to utilize new model for saving data -+ Added translation functions to all strings in coupon settings screen -+ Added new coupon settings - + _Expiration Date_ -- coupons cannot be applied to a purchase after the expiration date - + _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. - + _First Payment Discount_ -- Applies only to recurring payment coupons. Determines the discount applied to the first payment of a recurring payment transaction. - + _Recurring Payments Discount_ -- Applies only to recurring payment coupons. Determines the discount applied all payments (other than the first) of a recurring payment transaction. - + _Description_ -- Record internal notes for a coupon visible only by admins on the admin panel -+ 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. - -##### Orders - -+ Added Order Statuses - + Completed - Single payment only. Denotes a successful transaction - + Active - Recurring only. Denotes the subscription is active with no issues - + Expired - Recurring only. Denotes the subscription has ended and is no longer active - + Refunded - Denotes the order has been refunded. - + Cancelled - Denotes the order has been cancelled manually by an admin. - + Failed - Denotes payment has failed. For subscriptions a failed payment will switch from "active" to "failed" - + Pending - Denotes that the order has been created but payment has not been completed yet -+ Admin panel order table new features: - + The following columns are now sortable in ascending and descending orders: Order, Product, and Date - + Added totals based on order type (single or recurring) to the "Total" column - + Added an order status column for quick status review -+ Order notes available for internal and system notes. powered by WP comments. lots of inspiration (and code) from WooCommerce, thank you <3 -+ Added a bunch of currency settings (as well as right-side currency and decimal-less currency support!) - -##### New Templates - -+ __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" -+ __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 -+ __Course Prerequisite Template__ at "templates/course/prerequisites.php" displays prerequisite information (course and tracks) for a given course. -+ __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 -+ Significantly updated checkout process with all kinds of new templates including: - + templates/checkout/form-gateways.php - + templates/checkout/form-summary.php -+ __Unified "Lesson Preview"__ at "templates/course/lesson-preview.php" displays "buttons" in course syllabus (on course page) and in course navigation (on lesson pages) -+ Various template hook priority changes in order to make adding content between default LifterLMS areas easier - -##### Deleted Templates - -+ templates/checkout/form-checkout-cc.php -+ templates/checkout/form-pricing.php - -##### New & Updated Admin Interfaces & Templates - -+ Significantly improved, changed, or brand new templates for metaboxes for various post types: - + templates/admin/post-types/order-details.php - + templates/admin/post-types/order-transactions.php - + templates/admin/post-types/product-access-plan.php - + templates/admin/post-types/product.php - -##### New Functions - -+ `llms_confirm_payment_url()` - Retrieve the URL used for confirming LifterLMS Payments -+ `llms_cancel_payment_url()` - Retrieve the URL users are directed to when cancelling a payment - -##### Install Script - -+ Removed some legacy default options that were being created and are no longer required for new installations. -+ Removed unused `update_courses_archive()` function & related hook - -##### Select2 - -Now utilizing a forked version of Select2 to prevent 3.5.x conflicts we've been plagued with - -##### Deprecated - -+ Removed filter `lifterlms_get_price_html`, use `lifterlms_get_single_price_html` instead -+ Removed unused `LLMS_Product->get_price_suffix_html()` function -+ Removed `LLMS_Product->set_price_html_as_value()` because we didn't like it anymore, don't use anything instead. -+ Removed `add_query_var_course_id()` function -+ Removed `displaying_sidebar_in_post_types()` function with the `LLMS_Sidebars::replace_default_sidebars()` function -+ Filter `lifterlms_order_process_pending_redirect` has been replaced with `lifterlms_order_process_payment_redirect` -+ Action `lifterlms_order_process_begin` has been deprecated -+ Removed `lifterlms_order_process_complete` action -+ Replaced `LLMS_Course::check_enrollment()` with various new utilities. See `llms_is_user_enrolled()` for fastest use. -+ Officially removed the `LLMS_Language` class -+ 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. -+ Removed function `lifterlms_template_single_price()` and replaced with `lifterlms_template_pricing_table()` -+ Removed templates at "includes/course/price.php" and "includes/membership/price.php" in favor of "includes/product/pricing-table.php" -+ Removed `LLMS_Person::create_new_person()` in favor of `LLMS_Person_Handler::register()` or `llms_create_new_person()` -+ Removed `LLMS_Person->set_user_login_timestamp_on_register()` and are simply adding the metadata during registration -+ Removed `lifterlms_register_post` action hook which fired after new user registration validation, this has been replaced with `lifterlms_user_registration_after_validation` -+ Removed `lifterlms_new_person_data` and `lifterlms_new_person_address` filters, replaced with `lifterlms_user_registration_data` -+ Removed `LLMS_Person::login_user()` in favor of `LLMS_Person_Handler::login()` -+ background updater -+ system report facelift + inclusion of all new settings via `LLMS_Data` class -+ Fix setup wizard styles to follow update admin panel styles -+ add links to last step of setup wizard for documentation and demo -+ removed a bunch of deprecated coupon-related functions -+ added a "force ssl" option to ensure checkout is secured -+ added settings and options around recurring payments and staging sites to prevent duplicate charges when testing on a cloned site -+ Check course restrictions automatically when checking lesson -+ Added user_id to all access function checks to allow for checks for non current user -+ course restriction messages display regardless of enrollment status -+ check memberships and lock purchase of members only access plans -+ Fixed titles of course closed and open messages on the course restrictions options -+ record a start date for access plans based off when order moves to complete or active for the first time -+ automatically expire limited access plans -+ gave a quick facelift & unification to a lot of admin panel elements -+ Color consistency updated according to LLMS brand guide -+ Unified front and backend button classes -+ Updated all frontend buttons to have consistent classes -+ Removed the "FREE" lesson SVG in favor of simple text which allows translating -+ Install & activation overhauls. Resolves [#179](https://github.com/gocodebox/lifterlms/issues/179) -+ jQuery MatchHeight lib unignored -+ A bunch of settings pages updated and a bunch of settings deprecated -+ Gateways setting page removed -+ Memberships & Courses page combined into "Catalogs" settings -+ Added a data getting class used by the tracker class -+ added a new page creation function with better intelligence that (hopefully) prevents duplicate pages from being created during core page installation -+ new default country setting -+ all order status changes recorded as order notes -+ pending orders can be completed after failed payments -+ better handling for gateways with fields -+ JS spinners support multiples via start & stop! -+ Updated (and semi-finished) analytics -+ achievement metabox converted -+ minor updates to voucher class -+ Added a "post state" visible on the Pages posts table identifying if the page is saved as a LifterLMS page (EG: Checkout Page) -+ Fixed copy/paste error of duplicate enrollment closed message on course restrictions tab -+ Removed WC integration in favor of WC -+ Upgrade "back to course" template to new lesson API -+ Renamed `course/parent_course.php` to `course/parent-course.php` for template naming consistency -+ use `strict` when auto generating usernames when creating from email addresses, resolves [#182](https://github.com/gocodebox/lifterlms/issues/182) - -##### 3.0.0 Auto Upgrader - -+ lots of postmeta data rekeyed -+ intelligently generated defaults for various pieces of new meta data on courses, lessons, and memberships -+ automatically generate access plans from existing course and membership data -+ update existing orders to pull semi-accurate data into analytics based on new database structure -+ cleans database of a ton of deprecated options and postmeta data - -##### Deprecated - -+ function `llms_is_user_member()`, use `llms_is_user_enrolled()` instead -+ function `llms_check_course_date_restrictions()` -+ function `quiz_restricted()` -+ function `membership_page_restricted()` -+ function `is_topic_restricted()` -+ function `llms_get_post_memberships()` -+ function `llms_get_parent_post_memberships()` -+ function `parent_page_restricted_by_membership()` -+ function `outstanding_prerequisite_exists()` -+ function `find_prerequisite()` -+ function `llms_get_course_enrolled_date()` -+ function `llms_get_lesson_start_date()` -+ function `lesson_start_date_in_future()` -+ function `page_restricted_by_membership_alert()` -+ function `llms_does_user_memberships_contain_course()` -+ class `LLMS_Checkout` -+ function `LLMS()->checkout()` - -##### Auto Enrollment - -+ Course auto enrollment for Memberships has been restored -+ 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 - -##### Analytics - -+ 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. -+ Updated styles & interface - -##### bbPress - -+ Restrict individual forums (and their topics) to LifterLMS Membership levels - -##### BuddyPress - -+ Fixes broken course display on bp profile -+ Adds memberships subpage to bp profile - -##### notices - -+ Admin notices class for managing admin notices, it's pretty neat! - -##### Student Management on Courses and Memberships - -+ All new and improved student management interface for managing student enrollments from courses and memberships - -##### Deprecated - -+ filter: `llms_meta_fields_course_main`, replace with `llms_metabox_fields_lifterlms_course_options` - -##### Manual Payments - -+ Manual Payment Gateway can now be enabled on the frontend! -+ 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 -+ Define the payment instructions on the admin panel "Checkout Settings" -+ Once you verify payment, head to the pending order and hit the "Record a Manual Payment" button to record the transaction -+ Upon recording the order status will be upgraded to "Complete" and the user will be enrolled automatically - -##### Student Dashboard Upgrades - -+ More sane template hooks and functions -+ Pagination on Courses endpoint (view only a preview on the main dashboard) -+ Orders history & view orders screens! - -Deprecated options (and related functions where applicable) for the following course & membership options: - - + `lifterlms_button_purchase_membership_custom_text` - + `lifterlms_course_display_outline_lesson_thumbnails` - + `lifterlms_course_display_author` - + `lifterlms_course_display_banner` - + `lifterlms_course_display_difficulty` - + `lifterlms_course_display_length` - + `lifterlms_course_display_categories` - + `lifterlms_course_display_tags` - + `lifterlms_course_display_tracks` - + `lifterlms_lesson_nav_display_excerpt` - + `lifterlms_course_display_outline` - + `lifterlms_course_display_outline_titles` - + `lifterlms_course_display_outline_lesson_thumbnails` - + `lifterlms_display_lesson_complete_placeholders` - + `redirect_to_checkout` - -In all scenarios either a `add_filter` (returning false) or a `remove_action()` can be used to replicate the option. - - -v3.0.0-beta.4 - 2016-09-01 --------------------------- - -+ fix issue with course prereq checks -+ next payment due date visible on order admin view -+ trial end date visible on order admin view - -##### Free Access Plans - -+ "Free" access plans now defined as such based on a checkbox rather than by entering 0 into the price -+ 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) -+ trials are disabled with free plans (because trials only apply to recurring plans) -+ sales are disabled for free access plans - -##### Checkout Form JS API - -+ unified JS checkout handler -+ allows extensions to enqueue validation or pre-submission JS functions that should run prior to checkout form submission - -##### Manual Payment Gateway - -+ handles purchase of access plans marked ar FREE & orders that are discounted to 100% via coupons - -##### Open Enrollment - -+ Open Enrollment allows users to register on the account dashboard without purchasing a course -+ Voucher settings are available to customize whether vouchers should be optional or required during open registration -+ Better error reporting around voucher usage during enrollment - -##### Deprecated Functions - -+ `llms_get_coupon()` -+ `get_section_id()` -+ `check_course_capacity()` - -##### Quizzes - -+ Updated admin metaboxes to use new metabox abstract class -+ display 0 instead of negative attempts on quiz summary -+ updated logic in start button template - -##### Emails (for engagements) - -+ Admin metabox updated to new API -+ Postmeta data migration: - + `_email_subject` renamed to `_llms_email_subject` - + `_email_heading` renamed to `_llms_email_heading` - - -v2.7.12 - 2016-09-22 --------------------- - -+ Added a new filter on content returned after port permission checks -+ Added additional information to plugin update message in preparation for major 3.0 release -+ Updated plugin contributor metadata - - -v2.7.11 - 2016-07-22 --------------------- - -+ Removed a duplicate action hook on course archive loop. -+ Switched registration template include to use a more sane function -+ Added updated banner adds with prettier ones. Wooooooo. - - -v2.7.10 - 2016-07-19 --------------------- - -+ Fix undefined noticed related to LifterLMS custom post type archive filtering -+ Fix filter which was supposed to allow custom engagement types to be queried & triggered by engagements automatically but was passing data incorrectly - - -v2.7.9 - 2016-07-11 -------------------- - -+ We are now properly storing delayed engagement trigger data. -+ 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 -+ Fixed an undefined property notice related to email engagements when the email had no subject or header -+ Fixed a typo in the description of a translation function. -+ 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. -+ 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. - - -v2.7.8 - 2016-07-05 -------------------- - -+ Bugfix: Restore access to quiz results on quiz completion - - -v2.7.7 - 2016-07-01 -------------------- - -##### Russian - -+ LifterLMS is now 100% Translated into Russian thanks to our new Russian Translation Editor [@kellerpt](https://profiles.wordpress.org/kellerpt/) - -##### l18n - -+ All transition messages between questions during a Quiz are now translatable. -+ 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. - -###### Bug fixes - -+ 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. -+ 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). -+ Added new logic to display an error message (instead of nothing) if there's an error during question loading. -+ Resolve issue with course progress bar when added to a quiz sidebar (assuming your theme has sidebar support on your quizzes). -+ Updated version number in the changelog for last version (it was supposed to be 2.7.6) - - -v2.7.6 - 2016-06-28 -------------------- - -+ Students manually removed by Memberships by using the "Students" tab of a LifterLMS Membership will now be fully removed from the membership. -+ Updated a few time-related strings to be l18n friendly. These items were all around Quiz time reporting and quiz time limits. -+ Updated testing information, tested up to WP 4.5.3 -+ Fixed date of last release on changelog. It had the wrong date. Does that really matter? -+ Updated readme.txt description area, we have a new youtube video! Yassss. - - -v2.7.5 - 2016-06-13 -------------------- - -##### New features -+ Added an "id" parameter to both LifterLMS Courses and LifterLMS Memberships shortcodes - -##### i18n -+ Allow date translation on quiz results screen by using `date_i18n()` instead of `date()` -+ Allow date translation on my courses screen by using `date_i18n()` instead of `date()` -+ Ensure course status "Enrolled" is translatable on my courses screen - -##### Fixes -+ 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. -+ Resolved an issue where formatting for "Restricted Access Description" course content would not display proper formatting. -+ Fixed an issue with the "FREE" stamp for a free lesson caused layout issues. -+ Removed the "is-complete" css class from incorrectly being added to lesson preview tiles for free lessons -+ Fix an escaping issue when rendering Course titles inside LifterLMS notices. Prevents "\'s" from displaying when "'s" should be displaying (and similar issues). - - -v2.7.4 - 2016-05-26 -------------------- - -+ Fixed a bug with the new localization methods from 2.7.3 -+ 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). -+ Removed bundled en_US translation files because LifterLMS is in English so the files are unnecessary. -+ Fixed a few mis-labeled filters applied when registering LifterLMS Custom Post Types -+ Adjusted the default supported features of LifterLMS Quizzes and Questions - + Quizzes now support custom fields as per user request - + Commenting, thumbnails, and excerpts are no longer "supported" as they were never intended to be and were never correctly implemented. - + 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()` - - -v2.7.3 - 2016-05-23 -------------------- - -+ Added a separate filter for login redirects `lifterlms_login_redirect` and added the user_id as a second parameter available to the filter -+ Added second parameter to `lifterlms_registration_redirect` to allow access to the registered users's user_id -+ 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. -+ Added a "Pointer" when hovering quiz summary accordion to allow for a slightly more obvious user experience that the elements are expandable. -+ 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. - - -v2.7.2 - 2016-05-19 -------------------- - -+ In course syllabus widget & shortcodes free lessons will now be clickable links. -+ Record `llms_last_login` timestamp in usermeta when a user registers. - - -v2.7.1 - 2016-05-09 -------------------- - -##### Enrollment & Voucher Checks - -+ 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. -+ 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. -+ `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. - -##### Translation - -+ 3 strings have had translation functions added to them. This makes LifterLMS voucher redemptions translatable! - -##### Bugs & Fixes - -+ Fix javascript dependency & enqueueing issue on admin panel which prevented LifterLMS settings from saving correctly in various places -+ 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. - - -v2.7.0 - 2016-05-05 -------------------- - -##### LifterLMS Custom User Fields Exposed - -+ Custom fields added during registration via LifterLMS account settings are now exposed on the admin panel via the student's WordPress user profile -+ 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. -+ 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) - -##### Membership Manual Add & Remove Student Functions - -+ Duplicated "Students" tab from the Course admin screen to Memberships - + Students can be manually added to a membership by an admin - + Students can be removed manually from a membership by an admin - -##### Updates - -+ 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. - -##### Fixes - -+ Fixed a few spelling errors on LifterLMS admin panel order screens -+ Fixed a typo on meta data for LifterLMS admin created (manual) orders - - -v2.6.3 - 2016-05-02 -------------------- - -+ Removed redirecting action from WooCommerce integration that was causing issues on multiple product purchase checkouts with larger databases. -+ 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. -+ Fixed an issue with LifterLMS certificate background image that caused the wrong dimensions to be returned when outputting a LifterLMS certificate background image - - -v2.6.2 - 2016-04-27 -------------------- - -+ Fix class conflict in collapsible course outline widget template which caused some UX issues. -+ Added new filters run during course & lesson sidebar registration to allow customization of LifterLMS sidebars - + `lifterlms_register_course_sidebar` - + `lifterlms_register_lesson_sidebar` -+ Removed a stray logging function. -+ Cleaned up some undefined variable warnings & notices on the quiz summary template -+ Fixed an issue appearing when registering users did not submit the optional phone number which caused a PHP notice -+ 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. - - -v2.6.1 - 2016-04-26 -------------------- - -+ Fix class conflict in collapsible course outline widget template which caused some UX issues. - - -v2.6.0 - 2016-04-25 -------------------- - -##### Collapsible Course Outline Widget - -+ By request we've added an option to make your course outline widgets collapsible! -+ View feature [Documentation](https://lifterlms.com/docs/course-syllabus-widget/) -+ New translations available related to feature. I think it's 4 strings. - -##### Bug Fixes - -+ 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. -+ Resolved an issue that prevented post update, save, and publishing messages for core post types (posts, pages) from displaying properly. - - -v2.5.1 - 2016-04-22 -------------------- - -+ Fixed session handler initialization as it was being initialized prior to user data availability. -+ 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)** -+ 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? -+ 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 - - -v2.5.0 - 2016-04-15 -------------------- - -**Admin Panel Order Table Updates** - -+ Several visual improvements to the table -+ Exposed the following fields on the table - + Order number - + Customer name (with a link to their WP profile) - + Customer email (mailto link) - + Payment gateway used (this is filterable per gateway as well so gateways can improve the functionality here in the future) -+ Added a link to the product edit page from the product column -+ Free orders will now display as "Free" as opposed to {currency}0.00 -+ Removed the not-so-useful "Order" column which was a long ugly string of data that was displayed in other columns already -+ 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. -+ Numerous strings that were previously not translatable have been made translatable on this screen -+ A few new strings that previously didn't exist are now available for translation - -**Fixes and other small changes** - -+ Fixed a translation issue on the LifterLMS menu that we thought we fixed in the last release but have now really fixed (probably). -+ Fixed a few small issues with engagements as they related to external engagements triggered by other plugins and LifterLMS extensions. -+ 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. -+ Fixed various javascript issues, mostly removed `console.log()` statements. -+ Fixed a spelling error on the membership admin panel settings screen - - -v2.4.1 - 2016-04-07 -------------------- - -+ Tested and compatible with WordPress 4.5 Release Candidate. -+ 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. -+ 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. -+ Italian translation updates courtesy of [@AndreaBarghigiani](https://github.com/AndreaBarghigiani) -+ 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. -+ Adjusted some static functions to be non static in `class.llms.post-types.php` -+ Added a function to ensure support for post thumbnails on LifterLMS custom post types -+ 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. -+ Fixed undefined variable warning on quiz summary screen -+ 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) -+ resolved an undefined variable warning resulting from courses still holding a reference to a membership after the membership has been deleted or trashed - - -v2.4.0 - 2016-03-29 -------------------- - -##### Performance Improvements on the LifterLMS Quiz Builder - -+ 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. -+ Fixed a bunch of undefined variables that would produce PHP warnings in various quiz templates -+ Added validation to quiz questions on the admin panel to prevent the same question from being added to a quiz multiple times. -+ Fixed an issue that prevented quizzes from correctly marking the lesson as completed when the quiz was passed. -+ Added three new actions now available for developers to hook into. - + `lifterlms_quiz_completed` called upon completion of a quiz (regardless of grade) - + `lifterlms_quiz_passed` called when a quiz is completed with a passing grade - + `lifterlms_quiz_failed` called when a quiz is completed with a failing grade -+ Course Progress and Course Syllabus shortcodes (and widgets) now work on Quiz pages -+ 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` -+ Added validation to the Quiz general settings - + Cannot only enter numbers in attempts, percentage, and time limit fields - + Cannot enter a negative number or a number greater than 100 in the percentage field -+ Removed the membership restriction metabox from quiz admin and question admin screens - -##### Other fixes - -+ Fixed an issue that caused multiple certificates awarded for the same Course or Lesson to not properly display on the My Account page. -+ 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) -+ 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. -+ 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. -+ Fixed an AJAX related issue that was incompatible with PHP7 -+ Added the ability to have a "max" value on LifterLMS Admin Metabox number fields - - -v2.3.0 - 2016-03-24 -------------------- - -##### Engagements Refactoring (lots of bugfixes, performance improvements, more hook & filter friendly) - -+ We've completely rewritten the LifterLMS Engagement Handler methods (`class LLMS_Engagements`) and added some new engagement actions. -+ The rewrite unifies engagement handling into one function that can be easily hooked into by plugin and theme developers. -+ We've moved any engagement related data out of the main `LifterLMS` class -+ 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. -+ 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. -+ Removed the unused `LLMS_Engagements` class and file -+ Added two new engagement trigger events "Membership Purchased" and "Course Purchased" -+ 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). - + `lifterlms_lesson_completed_notification` - + `lifterlms_section_completed_notification` - + `lifterlms_course_completed_notification` - + `lifterlms_course_track_completed_notification` - + `lifterlms_course_completed_notification` - + `lifterlms_user_purchased_product_notification` - + `lifterlms_created_person_notification` - -##### Bug and Issue fixes - -+ 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. -+ Fixed a CSS issue that caused some weirdness on the course archive page on mobile devices -+ Fixed an issue with automated membership expirations -+ Fixed a function that should have been called statically in `LLMS_Ajax` class -+ Fixed a ton of issues related to the triggering of engagements and cleaned up a lot of classes and functions associated with them. -+ Properly instantiate `LifterLMS` singleton via LLMS() function and prevent direct instantiation of the class via `new LifterLMS()`. -+ Removed the deprecated 'class.llms.email.person.new.php' file as it was rendered useless a long time ago and caused some duplicate emails. - - -v2.2.3 - 2016-03-15 -------------------- - -##### Translations - -+ Added translation functions around quite a few untranslated strings. Thanks to the team at [Netzstrategen](http://netzstrategen.com) -+ Added German translation .mo and .po files again thanks to the team at [Netzstrategen](http://netzstrategen.com) - -##### Student Enrollment Functions - -We've refactored a bit of our code related to how to programmatically enroll a student in a course or membership during registration and purchase. - -A 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. - -We'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. - -The enrollment function calls a new action as well as calling existing enrollment-related actions: - -+ `before_llms_user_enrollment` - called immediately prior to beginning the user enrollment function -+ `llms_user_enrolled_in_course` (previously existing) -+ `llms_user_added_to_membership_level` (previously existing) - -This 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. - -##### Bug and Issue fixes - -+ 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. -+ New lines are now preserved in the quiz question clarification text areas, thanks to @atimmer -+ Escape HTML in the quiz question description fields on the admin panel to allow outputting html without rendering it, thanks @atimmer -+ Fixed an issue related to the outputting of restricted course and membership content which caused errors on certain themes -+ added a clearfix to the `.llms-lesson-preview` element on the course syllabus template -+ Removed the `class.llms.person.handler.php` file as it wasn't actually being used by anything anywhere and contained no functions -+ Removed some unused and deprecated class functions from the LLMS Student Metabox class -+ 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. - - -v2.2.2 - 2016-03-15 -------------------- - -##### One step closer to a public GitHub repository - -We'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. - -Okay, 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/). - -Notable 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. - -##### Quizzes - -+ Created new time calculation and humanizing functions related to the display of quiz time on quiz results pages -+ Quizzes will now display hours, minutes, and seconds depending on the time it took to take the quiz -+ Timing calculations are more accurate and quizzes that are completed in less than 60 seconds will not bug out and display incredibly long lengths -+ Resolved an issue that occasionally prevented quiz data from saving during the last question causing the quiz to hang in an uncompletable state -+ Quiz questions now have a default point value of 1, thanks @atimmer -+ Quiz question answers now accept valid HTML as per `wp_kses_post`, thanks again to @atimmer - -##### Translations - -+ Thanks to @AndreaBarghigiani and the team at [codeat](http://codeat.co/) LifterLMS now ships with Italian language files! - -##### Issue and bug resolutions - -+ Fixed a restriction issue that would happen when individual lessons were restricted to a membership level -+ Fixed an issue with the `[lifterlms_my_account]` shortcode that was preventing the shortcode from working on the Divi theme. -+ Engagements will now only be triggered if they are "Published". Resolves an issue where draft or trashed engagements were still firing. -+ Fixed CSS overflow on LifterLMS Meta boxes. Fixes an issue where select boxes would be hidden inside a metabox. -+ 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. -+ Added a link to the new ConvertKit extension to the .org readme -+ 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. -+ CSS fix for `has-icon` class on course syllabus -+ Fixed a PHP warning that displayed when purchasing a membership with no auto-enrollment courses -+ Fixed an undefined variable warning in the WooCommerce integration class -+ Fixed a few templating issues related to certificates -+ Added a few new CSS rules that should make certificates more compatible across various themes -+ Added a css class to LifterLMS Next Lesson buttons, `llms-next-lesson` -+ 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. - - -v2.2.1 - 2016-03-07 -------------------- - -+ Added a few actions to the `class.llms.voucher.php` class. - - -v2.2.0 - 2016-03-04 -------------------- - -##### Translations - -+ 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. -+ We've also made it easier to include custom translations. Read our [Translation Guide](https://lifterlms.readme.io/docs/getting-started-with-translation). - -##### Certificate Background Images - -_We've completely rewritten the certificates template (but it's all backwards compatible)._ - -+ 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). -+ 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/). - -##### Course and Membership Pricing & Sales - -+ Sale price start and end date are now completely optional. - + Provide neither a start date nor an end date to have a sale run indefinitely - + Provide a start date with no end date to have a sale start at a pre-determined time with no pre-determined ending - + Provide an end date with no start date to have a sale end a a pre-determined date but start immediately - + Provide a start date and an end date to have a sale run for a pre-determined period of time -+ Optimized the `LLMS_Product` class to provide more reliable and extendable use of the class -+ The templates related to pricing functions have been refactored. Affected templates include: "templates/course/price.php", "templates/loop/price.php", "templates/membership/price.php" -+ 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. - -##### Coupon Updates - -+ Coupons can (finally) be removed after being applied! -+ Coupons can now be restricted to specific courses and/or memberships -+ Percentage based coupons can no longer be created with a value larger than 100% -+ Added numeric restrictions to usage and coupon amount fields on the admin panel -+ Fixed a programmatic error that prevented product restrictions from being entirely removed -+ Fixed a few instances where hardcoded a US Dollar symbol ($) where a dynamic currency symbol should have been displayed. - -##### Wow Bad Syntax, Very Typo, Such Grammar, So Undefined - -+ Fixed a typo in filter associated with modifying the registration of the lesson post type (`lifterlms_register_post_type_lesson`) -+ Fixed a grammatical error in a Membership restriction message -+ Fixed a syntax error in "/templates/course/outline-list-small.php" that prevented the `done` CSS class from being properly applied to completed lessons -+ Fixed a few typos and grammatical errors on the Course and Membership settings metaboxes -+ Fixed an undefined variable in "templates/course/syllabus.php" -+ Fixed an issue on the system report that prevented the "Courses Page" from being reported properly -+ Fixed an issue that caused PHP warnings on the admin panel for students or WP users with no LifterLMS menu permissions -+ Fixed an installation warning caused by a reference to an undefined class variable -+ Fixed an HTML character encoding issue that caused `–` to display on the admin panel when viewing LifterLMS Orders -+ Fixed an undefined variable found during engagement triggering for non-email engagements. - -##### Additional, less exciting updates - -+ Added input type restrictions to course & membership price fields. -+ 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. -+ Added `the_content` filter to the content of emails sent by LifterLMS -+ Fixed some CSS issues on Voucher screens -+ Updated Courses settings retrieval function to retrieve the correct "shop" page id -+ Added translation functions to voucher export meta box class -+ Vouchers Export metabox will only allow export after a voucher has been published. This prevent's an issue caused by attempting to export voucher codes before they were saved in the database via the publish / save action. -+ Vouchers can no longer be saved with a use of "0" -+ 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. -+ Removed the membership restriction metabox from some post types where it shouldn't have been displaying. -+ 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 - - -v2.1.1 - 2016-02-15 -------------------- - -##### System Report - -+ A new LifterLMS Admin Page is available which reports information about various server, WordPress, and LifterLMS settings that will help expedite support requests. -+ 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/) - -##### Additional Updates - -+ Fixed a javascript issue which prevented users from saving vouchers -+ Cleaned up formatting in a large number of included PHP files - - -v2.0.5 - 2016-02-15 -------------------- - -+ 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. -+ 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. - - -v2.0.4 - 2016-02-15 -------------------- - -+ Fixed a typo on the `class_exists` check in the deprecated functions file -+ added a filter so that progressive users can opt out of loading the deprecated functions file - - -v2.0.3 - 2016-02-12 -------------------- - -+ Removed an unused quiz stub - - -v2.0.2 - 2016-02-11 -------------------- - -+ Bugfix: removed a progressive syntax array that caused fatal errors on older versions of PHP - - -v2.0.1 - 2016-02-11 -------------------- - -##### Updated General Settings Screen - -+ 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. -+ Moved Currency options to the Checkout settings screen - -##### Bug Fixes - -+ Properly initialized jQuery on the vouchers metabox admin scripts -+ removed some php shortcut echos (`<?= $var; ?>`) -+ 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. -+ Fixed a few files where undefined variables were being referenced and generating php notices -+ removed an call to a WordPress core function that has never existed. Not sure what we were thinking there... - -###### Enhancements - -+ Updated CSS to provide better course syllabus layout on smaller screens -+ Added validation to prevent against duplicate voucher code creation - - -v2.0.0 - 2016-02-04 -------------------- - -##### Auto-advancing lessons - -+ 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. - -##### Bug Fixes - -+ Added spaces between numbers and "of" on the counter for course syllabus templates -+ Removed a template hook that was creating duplicate lesson thumbnails on quite a few themes - -##### Membership Admin Improvements - -Visit the "Enrollment" tab on any membership to see some new additions to make managing your memberships easier. - -+ You can now add courses to and remove courses from a Membership from the Membership itself -+ 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 - -##### Student Enrollment & Removal on Courses Admin Screen - -We've updated the Students tab interface for performance and usability! - -+ AJAX enabled searching by student name and or email -+ 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. -+ Allow for addition or removal of several students at a time. - -##### Syllabus Template - -+ Added a Course setting to optionally enable Lesson Thumbnails on the Course Syllabus -+ Added a Course setting Display greyed out lesson completion checkmark icons on lessons not competed in the course syllabus -+ Reworded CSS on the course syllabus to rely on floats rather than absolute positioning, should allow for more robust customization with less frustration -+ Refactored the syllabus template at "templates/course/syllabus.php" for better performance and readability - -##### Updates and enhancements - -+ User email is now displayed on the "Students" table on student analytics screens -+ Membership now has it's own admin menu -+ Reordered the LifterLMS admin menu and submenu items -+ Removed membership specific taxonomies from courses -+ Removed course specific taxonomies from memberships -+ Coupon code is now a required field when creating a coupon -+ "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. - -##### Deprecated Classes - -We'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! - -+ `LLMS_Activate` which as previously used to activate the plugin for updates via the LifterLMS Update Server and is no longer required. -+ PUC (plugin update checker) Library has been completely removed as it is no longer required for plugin updates. -+ `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! - -##### Deprecated Functions - -+ `lifterlms_template_section_syllabus()` - -**The following are officially deprecated and removed to prevent WooCommerce compatibility conflicts** - -+ `is_shop()` replaced by `is_llms_shop()` -+ `is_account_page()` replaced by `is_llms_account_page()` -+ `is_checkout()` replaced by `is_llms_checkot()` - -##### Deprecated Templates - -+ templates/course/section_syllabus.php - -##### New Account Dashboard Filters - -*[View documentation for more information](https://lifterlms.readme.io/docs/filters-account)* - -+ `lifterlms_account_greeting` -+ `lifterlms_my_courses_title` -+ `lifterlms_my_courses_enrollment_status_html` -+ `lifterlms_my_courses_start_date_html` -+ `lifterlms_my_courses_course_button_text` -+ `lifterlms_my_certificates_title` - -##### New Checkout Page Filters: - -*[View documentation for more information](https://lifterlms.readme.io/docs/filters-checkout)* - -+ `lifterlms_checkout_user_logged_in_output` -+ `lifterlms_checkout_user_not_logged_in_output` - -##### New Course Filters: - -*[View documentation for more information](https://lifterlms.readme.io/docs/filters-course)* - -+ `lifterlms_product_purchase_account_redirect` -+ `lifterlms_product_purchase_redirect_membership_required` -+ `lifterlms_product_purchase_checkout_redirect` -+ `lifterlms_product_purchase_membership_redirect` -+ `lifterlms_lesson_complete_icon` - - -v1.5.0 - 2016-01-22 -------------------- - -##### WooCommerce Integration Enhancements - -__NOTE: The following enhancements only apply when the WooCommerce Integration is enabled__ - -**Always redirect to the WooCommerce Cart when a SKU Matched Product can be found** - -+ 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. -+ If no WooCommerce product is found via a SKU match, the user will proceed to the LifterLMS checkout. -+ 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). - -**Multiple Item Checkout** - -+ 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. -+ The products in the order will be intelligently SKU matched to LifterLMS Courses or Memberships. -+ 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. - -##### Other Fixes and improvements - -+ Fixed a bug that caused quiz results to display for users who had never taken the quiz. -+ Added Wistia as an oEmbed provider to fix an issue related to default oembed handling in WordPress 4.4. -+ 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 -+ 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). -+ Tested and compatible up to WordPress 4.4.1 - -##### Deprecations - -**The following functions have been staged for deprecation in LifterLMS 2.0!** - -+ 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. -+ 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. - - -v1.4.5 - 2016-01-13 -------------------- - -+ 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.** -+ Allow "Payment Method" to be translated on the "Confirm Payment" screen -+ Allow the name of the payment gateway to be filtered on the "Confirm Payment" screen -+ Added pagination support to lifterlms membership archive pages -+ Fixed a bug related to some required global variables for quizzes and lessons being incorrectly set on certain hosts -+ updated readme file to remove incomplete documentation -+ Added Chosen multi-select options to admin panel metaboxes (settings and posts) -+ Added two new actions that developers can hook into: - + `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). - + `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). - - -v1.4.4 - 2015-12-21 -------------------- - -##### Updates - -+ My account page can now (optionally) display a list of memberships a student is currently enrolled in -+ Student analytics on the admin panel display student's Memberships -+ Student analytics on the admin panel will now display student's progress through courses in addition to their current enrollment status. -+ Custom taxonomy archive templates for Course tags, categories, tracks, and difficulties now exist and properly function. -+ Custom taxonomy archive templates for Membership categories and tags now exist and properly function. -+ 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) -+ Added basic styles to LifterLMS pagination HTML elements (elements with class `.llms-pagination`) which formerly had no associated CSS. - -##### Deprecations - -+ 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. - -##### Bug fixes - -+ Fixed pagination issues when using the `[lifterlms_courses]` shortcode -+ Fixed an issue with the `is_shop()` function that prevented courses per page option from functioning properly on the default course archive page -+ Student analytics profile on admin panel will display the correct number of memberships the student is enrolled in. -+ 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 - -##### Miscellaneous - -+ Account settings screen displays the correct title ("Account Settings" it previously said "Archive Settings") -+ Made language changes to the LifterLMS settings intro screen copy -+ Added link to CourseClinic on settings intro screen -+ Added link to LifterLMS documentation on the settings intro screen - - -v1.4.3 - 2015-12-11 -------------------- - -+ Fixed an issue that could prevent some older servers from being able to run LifterLMS - - -v1.4.2 - 2015-12-10 -------------------- - -+ Tested and compatible with WordPress version 4.4 -+ BugFixes: fixed issue in `llms_featured_img()` that was preventing the `$size` variable from being passed to the WP core function being utilized. -+ BugFixes: correctly handling conflicts with Plugin Update library - - -v1.4.1 - 2015-12-02 -------------------- -+ 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! -+ Feature: Custom Purchase Course Button Text Option. Change the text of the Take This Course button in Settings->Courses. -+ Feature: New Become A Member button on courses that are restricted to memberships. -+ Feature: Custom Become A Member Text Option. Change the text of the become a member button in Settings->Courses. -+ Feature: Paypal Debug Mode. Enable debug mode in Settings->Gateways to view responses from Paypal API when errors occur. -+ Updates: Updated support links in Settings->General. -+ Updates: added minor styling to course page to increase margin and padding for some themes. -+ 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. -+ BugFixes: Resolved issue with no default price selected at checkout when only recurring option existed. -+ BugFixes: Lesson prerequisite now alert the user and provide a link to redirect the user to the next required lesson in the course. -+ BugFixes: Paypal errors now return error message instead of white screen when Paypal API fails. -+ BugFixes: Corrected JavaScript error with modals on course edit page in Internet Explorer 11. - - -v1.4.0 - 2015-10-29 -------------------- -+ Feature: Free lessons - demo lessons that can be taken at any time by any user -+ Feature: Guest lessons - demo lessons that can be taken by a non-logged in user -+ Feature: Random quiz question - quiz questions can now be set to be in user set order or random order -+ Updates: Automatically registers appropriate sidebars for Genesis theme -+ Updates: Backend file cleanup -+ Updates: Text cleanup -+ Updates: Adds greater localization support (more strings to translate! yay!) -+ Updates: Cleans up some unnecessary console.log() calls -+ Updates: Removes mass of commented out code (cleaner reading) -+ Updates: 'Next Lesson' button added after successful completion of quiz -+ Updates: 'Next Lesson' button at bottom of lesson properly gets starting lesson of next section at the end of the previous section -+ Updates: 'Previous Lesson' button at bottom of lesson will now properly get last lesson of previous section (if applicable) -+ Updates: Move Registration Form to global templates to allow users to disable registration on login page but use registration form on custom page. -+ BugFixes: WordPress pages are now properly restricted by memberships -+ BugFixes: Fixes bug that caused order screen to act up if user was deleted -+ BugFixes: Resolves nasty little bug that caused syllabus numbers to be out of whack -+ BugFixes: Resolved error with WooCommerce integration where courses would not always register the user -+ BugFixes: Corrected CSS conflict with Bridge theme settings page - - -v1.3.10 - 2015-10-15 --------------------- -+ Updates: Clarifies some prerequisite text -+ Updates: Quiz questions are now randomized! -+ Updates: Fixes small CSS issue -+ BugFixes: Resolves fatal errors with a small subset of premium themes - - -v1.3.9 - 2015-10-5 ------------------- -+ BugFixes: Removes conflict with Yoast SEO -+ BugFixes: Fixes CSS issues with box-sizing takeover -+ Feature: New Settings Tile: Session Management. Found at LifterLMS->Settings->General. -+ Feature: Clear User Session Tool. You can now clear all LifterLMS user session data from your site in LifterLMS->Settings->General -+ Updates: Backend code cleanup - - -v1.3.8 - 2015-10-02 -------------------- -+ BugFixes: Fixes Random error notices -+ Updates: Updates email template handler - - -v1.3.7 - 2015-09-25 -------------------- -+ Updates: Adds Spanish translation -+ Updates: Adds new filter 'lifterlms_single_payment_text' to customize single payment string on checkout -+ Updates: Student analytics now indicate which courses a student has completed -+ BugFixes: Resolved security issue with WordPress searches and lessons -+ BugFixes: Fixes analytics bug that potentially arises after a course is deleted - - -v1.3.6 - 2015-09-18 -------------------- -+ BugFixes: Fixes pesky Zend Error that plagued some unfortunate victims -+ BugFixes: Students can now be properly deleted from the course -+ BugFixes: Fixes random class redeclaration error messages -+ Updates: Adds new filter 'lifterlms_quiz_passed' to customize 'Passed' text after quiz -+ Updates: Adds new filter 'lifterlms_quiz_failed' to customize 'Failed' text after quiz - - -v1.3.5 - 2015-09-11 -------------------- -+ Revisions: Fixes typos -+ Updates: Adds sidebar functionality to various themes - - -v1.3.4 - 2015-09-04 -------------------- -+ BugFixes: Fixes bug with featured image on course page -+ BugFixes: Fixes issue with lesson completed percentage on analytics page - - -v1.3.3 - 2015-09-01 -------------------- -+ Updates: Removes deprecated plugin updater -+ Updates: Adds Course Track prerequisite -+ Updates: Various text fixes -+ BugFixes: Fixes lesson name on prerequisite notification -+ BugFixes: Fixes critical error with WordPress customizer - - -v1.3.2 - 2015-08-30 -------------------- -+ Hotfix: resolves issues with sidebar shortcodes -+ Updates: Text clarifications - - -v1.3.1 - 2015-08-28 -------------------- -+ Hotfix: resolves issue with ajax url - - -v1.3.0 - 2015-08-28 -------------------- -+ Improved popover behavior in course creation. -+ BugFixing. Prevent multiple lesson and section form submission -+ Fixed typos at backend quiz page -+ Fixed check for update bug when plugin isn't properly activated. -+ BugFixing, quiz post type should show author metabox -+ Added course category filter to lifter_lms shortcode -+ BugFixing, typo in [lifterlms_course_progress shortcode] -+ BugFixing, Analytics shouldn't fetch students meta info from users were deleted. -+ Adds in basic review functionality -+ Updates plugin-updater to remedy PHP conflicts -+ Fixes date bug in Analytics -+ Cleans up jQuery console messages -+ Adds in course tracks - - -v1.2.8 - 2015-07-17 -------------------- -+ Updated Portuguese translation file -+ Fixed issue where quiz score could not be equal to required grade. -+ New Feature: Quiz Results Summary. Display the quiz results to the user on quiz completion. -+ New feature: Clarification. Display information about correct and incorrect answers to users -+ New Feature: Display correct answers to user on quiz completion -+ Removed ability to add negative time limit to quiz -+ 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. -+ Sidebar support for prototype theme -+ Sidebar support for X theme -+ Sidebar support for WooCanvas -+ 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"] -+ New updates to gulp build process -+ Class autoloading and LLMS namespace introduced for more efficient coding. - - -v1.2.7 - 2015-06-05 -------------------- -+ Minor bug fix with lesson redirect to quiz -+ Minor change to global Course object instantiation. -+ Bug Fix: Remove student from course -+ Bug Fix: Appearance Menus missing select field (THANKS ANDREA!) -+ New Course Setting: Hide Course Outline on course page -+ New Shortcode: [lifterlms_course_outline] - displays course outline with settings (see documentation) -+ Membership metabox design update -+ Certificate metabox design update -+ Achievement metabox design update -+ Lesson metabox design update -+ Emails metabox design update -+ Coupons metabox design update -+ Update to certificate design (better alignment and theme functionality) -+ Better theme sidebar support -+ More awesome control for developers building new settings for LifterLMS -+ Advanced filter system for metabox fields with finite control for 3rd party developers. -+ Woocommerce conflict correction to archive templates -+ Style updates to allow themes better control on design - - -v1.2.6 - 2015-04-28 -------------------- -+ Corrected issue with lesson re-order on save -+ corrected html formatting issue on purchase page -+ corrected html formatting issue on course page - - -v1.2.5 - 2015-04-23 -------------------- -+ Corrected excerpt to not pull in lesson navigation -+ Modified metabox api for better extension integration -+ Corrected issue with order not displaying all information if coupon was not applied to order - - -v1.2.4 - 2015-04-22 -------------------- -+ Moved All Course metaboxes to global Course Options Metabox -+ Move Enrolled and Non-Enrolled user wysiwyg post editors to Options Metabox -+ Removed Course Syllabus metabox, Added Course Outline Metabox -+ Set priority of Course Outline and Course Options Metabox to top -+ Added ability to Create new section to Course Outline -+ Added ability to Create new lesson to Course Outline -+ Added ability to add existing Lesson to Course Outline -+ Added Lesson duplicate functionality when adding lesson previously assigned to another course. -+ Added ability to drag lessons between sections in Course Outline -+ Added ability to edit Section Title in Course Outline -+ Added ability to edit lesson title and excerpt in Course Outline -+ Added New Style and Design for better usability to Course Outline -+ Added Lesson Icon with tooltip to Course Outline: Prerequisite - shows if prerequisite exists and displays name of prerequisite -+ Added Lesson Icon with tooltip to Course Outline: Quiz - shows if quiz is assigned to course and displays name of quiz -+ Added Lesson Icon with tooltip to Course Outline: Drip Content - shows if drip days are set and # of days -+ Added Lesson Icon with tooltip to Course Outline: Content - displays if lesson has content added. -+ 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. -+ Style Update: backgrounds on frontend. Removed all references to white background on front end elements -+ Corrected Restriction for course in past. Updated course in past message to display as Course ended instead of Course not available until. -+ Added restriction message when user attempts to visit a restricted lesson. -+ Updated course syllabus sidebar widget to not display lessons as links if user is not enrolled in course. -+ Added ability to use Attribute Order for sorting Courses and Memberships on Archive pages. -+ Added support for selling memberships with Woocommerce. LifterLMS now checks memberships for SKU matches in addition to Courses when products are purchased using WooCommerce. -+ Added gulp for scss, js and svg management -+ Added svg sprite and svg class for managing svg elements on front and backend. -+ Added better language translation support for strings -+ Refactored Ajax Classes for cleaner, faster development -+ Refactored metabox build class for cleaner, faster development -+ Refactored Course syllabus to reduce query size for larger, complex courses -+ Added Handler classes for Lessons, Sections, Courses and Posts -+ Refactored Course get / set methods to reduce database queries - - -v1.2.3 - 2015-03-12 -------------------- -+ Achievement design and functionality updates -+ Achievement shortcode added -+ Better searching added to engagement screen -+ Achievement bug fixes -+ On screen error reporting added to activation for trouble shooting -+ Custom engagement methods added to certificate, achievement and sections -+ Corrected new user registration engagement bug -+ LifterLMS access reduced from manage_options to edit_posts -+ Filters added to analytics to allow custom development -+ Engagement bug fix: Section and Lesson bug select -+ Syllabus bug corrected: No longer displays lessons in section box if no sections exist. -+ Removed depreciated achievement template -+ Membership Bug fix: Membership restriction will now only display on single posts. - - -v1.2.2 - 2015-02-23 -------------------- -+ Corrected drip content bug -+ Added Ajax functionality to quiz -+ rounded quiz grades -+ Added quiz time limit setting to Quiz -+ Added quiz timer to quiz, front end -+ Quiz allowed attempts field now allows unlimited attempts -+ Set Ajax lesson delete method to not return empty lesson value -+ Set next and previous questions to display below quiz question -+ Decoupled Single option select question type from quiz to allow for more question types -+ Added Quiz time limit to display on Quiz page -+ Added functionality to automatically complete quiz when quiz timer reaches 0 -+ Moved Quiz functionality methods from front end forms class to Quiz class - -v1.2.1 - 2015-02-19 -------------------- -+ Updated settings page theming -+ Added Set up Quick Start Guide -+ Added Plugin Deactivation Option -+ Updated language POT file -+ Added Portuguese language support. Thank you Fernando Cassino for the translation :) - - -v1.2.0 - 2015-02-17 -------------------- -+ Admin Course Analytics Dashboard Page. View at LifterLMS->Analytics->Course -+ Admin Sales Analytics Dashboard Page. View at LifterLMS->Analytics->Sales -+ Admin Memberships Analytics Dashboard Page. View at LifterLMS->Analytics->Memberships -+ Admin Students Search Page. View at LifterLMS->Students -+ Admin Student Profile Page ( View user information related to courses and memberships ) -+ Lesson and Course Sidebar Widgets ( Syllabus, Course Progress ) -+ Course Syllabus: Lesson blocks greyed out. Clicking lesson displays message to take course. -+ Misc. Front end bug fixes -+ Misc. Admin bug fixes -+ Course and Lesson prerequisites: Can no longer select a prerequisite without marking "Has Prerequisite" -+ Admin CSS updates -+ Better Session Management -+ Number and Date formatting handled by separate classes to provide consistent date formats across system -+ Zero dollar coupon management: Coupons that set total to 0 will bypass payment gateway, generate order and enroll users. -+ Better coupon verification. -+ Better third party payment gateway support. Third party gateway plugins are now easier to develop and integrate. -+ User Registration: Phone Number Registration field option now available in Accounts settings page. - - -v1.1.2 - 2014-12-18 -------------------- -+ Moved Sidebar registration from plugin install to init - - -v1.1.1 - 2014-12-16 -------------------- -+ Added user registration settings to require users to agree to Terms and Conditions on user registration -+ Added comments to all classes methods and functions -+ Removed unused and depreciated methods -+ Added Lesson and Course Sidebar Widget Areas -+ Fixed bug with course capacity option -+ Fixed bug with endpoint rewrite -+ Added localization POT file and us_EN.po translation file - - -v1.1.0 - 2014-12-08 -------------------- -+ Updated HTML / CSS on Registration form -+ Added Coupon Creation -+ Added Coupon support for checkout processing -+ Added Credit Card Support processing support -+ Added Form filters for external integration -+ Added Form templates for external integration -+ Added Account Setting: Require First and Last Name on registration -+ Added Account Setting: Require Billing Address on registration -+ Added Account Setting: Require users to validate email address (double entry) -+ Added password validation (double entry) on user registration / account creation -+ Added Quiz Question post type and associated metaboxes -+ Added Quiz post type and associated metaboxes -+ Added ability to assign a quiz to a lesson -+ Added front end quiz functionality -+ Added Course capacity (limit # of students) - -### User Admin Table -+ Added Membership Custom Column that displays user's membership information -+ Added "Last Login" custom column that displays user's last login date/time - -### User Roles -+ Updated user role from "person" to "student" -+ Added temporary migration function to transition any register users with "person" role to "student" role -+ Added "Student" role install function - - -### BUDDYPRESS -+ BuddyPress Screen Permission Fix -+ Added two additional screens to BuddyPress: Certificates and Achievements - -### MISC -+ Added llms options for course archive pagination and added course archive page pagination template -+ Added user statistics shortcode - - -v1.0.5 - 2014-11-12 -------------------- - -+ Fixed a mis-placed parenthesis in templates/course/lesson-navigation.php related to outputting excerpt in navigation option -+ Changed theme override template directory from /llms to /lifterlms -+ Update the position & name of the "My Courses" Menu in BuddyPress Compatibility file -+ New meta_key _parent_section added for easier connection and quicker queries. -+ Section sorting on course syllabus -+ Edit links added to course syllabus -+ Assign section to course and view associated lessons metabox added to sections -+ Assign lesson to section and view associated lessons metabox added to lessons -+ Assigned Course, Assigned Section, Prerequisite and Membership Required added to lesson edit grid -+ Assigned Course added to section edit grid' -+ New membership setting: Restrict Entire Site by Membership Level (allows site restriction to everything but membership purchase and account). -+ Updated template overriding to check child & parent themes -+ Updated template overriding to apply filters to directories to check for overrides to allow themes and plugins to add their own directories - - -v1.0.4 - 2014-11-04 -------------------- - -+ Templating bug fix -+ Added shortcode and autop support to course and lesson content / excerpt - - -v1.0.3 - 2014-11-04 -------------------- - -+ Major Templating Update! -+ Removed Course, Lesson and Membership single lesson templates. -+ Course and Section content templates now filter through WP content - - -v1.0.2 - 2014-10-31 -------------------- - -+ Added lesson short description to previous lesson preview links -- it was rendering on "Next" but not "Previous" -+ Added a class to course shop links wrapper to signify the course has been completed -+ Removed an unnecessary CSS rule related to the progress bar - - -v1.0.2 - 2014-10-30 -------------------- - -+ Fixed SSL certificate issues when retrieving data from https://lifterlms.com -+ Added rocket settings icon back into repo - - -v1.0.1 - 2014-10-30 -------------------- - -+ Updated activation endpoint url to point towards live server rather than dev - - -v1.0.0 - 2014-10-30 -------------------- - -+ Initial public release. diff --git a/README.md b/README.md deleted file mode 100644 index 439ed37f76..0000000000 --- a/README.md +++ /dev/null @@ -1,178 +0,0 @@ -<h1 align="center"> - <img src=".github/lifterlms-logo.png" alt="LifterLMS logo" width="300"> -</h1> - -<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> - -<hr /> - -<div align="center"> - -[![WordPress Plugin Version][img-wp-plugin]][link-wp-repo] -[![WordPress Plugin Tested WP Version][img-wp-tested]][link-wp-repo] -[![PHP Supported Version][img-php]][link-php] - -[![WordPress Plugin Rating][img-wp-rating]][link-wp-reviews] -[![WordPress Plugin Downloads][img-wp-downloads]][link-wp-advanced] -[![WordPress Plugin Active Installs][img-wp-installs]][link-wp-advanced] - -[![PHPUnit Tests][img-phpunit-tests]][link-phpunit-tests] -[![PHPCS Coding Standards][img-phpcs-checks]][link-phpcs-checks] -[![Code Climate maintainability][img-cc-maintainability]][link-cc] -[![Code Climate test coverage][img-cc-coverage]][link-cc-coverage] - -[![Contributions Welcome][img-contributions-welcome]](.github/CONTRIBUTING.md) -[![Contributors][img-contributors]](#contributors) -[![Slack community][img-slack]][link-slack] - -</div> - -<hr /> - -Welcome to the LifterLMS GitHub repository. This repository serves as the core project's central location for issue tracking and feature development. - -If you're not a developer or contributor, please use [LifterLMS plugin page][link-wp-repo] at WordPress.org. - - -### Getting Help and Support - -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][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]. - - -### Resources and Documentation - -+ [Changelog](./CHANGELOG.md) -+ User documentation and knowledge base: https://lifterlms.com/docs/ -+ Contributor's blog: https://make.lifterlms.com/docs/ -+ Developer portal: https://developer.lifterlms.com/docs/ - - -### Included Core Packages - -The 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. - -These packages have their own GitHub repositories: - -+ [LifterLMS Blocks](https://github.com/gocodebox/lifterlms-blocks) -+ [LifterLMS REST API](https://github.com/gocodebox/lifterlms-rest) - - -### Reporting a Bug - -Bugs can be reported at https://github.com/gocodebox/lifterlms/issues/new. - -Before 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. - -Also check our [known issues and conflicts](https://lifterlms.com/doc-category/lifterlms/known-conflicts/) for possible resolutions. - - -### Reporting a Security Vulnerability - -Security 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. - - -### Installing - -If you clone or download this repo directly it will not run as a plugin inside WordPress! - -Installable 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) - -If you're interested in installing development versions, see [Installing for Development](docs/installing.md) - - -### Contributing - -[![Contributions Welcome][img-contributions-welcome]](.github/CONTRIBUTING.md) - -Interested in contributing to LifterLMS? We'd love to have your contributions. Read our contributor's guidelines [here](.github/CONTRIBUTING.md). - - -### Contributors - -[![Contributors][img-contributors]](#contributors) - -Endless thanks to all our incredible contributors! - -[//]: contributor-faces -<a href="https://github.com/thomasplevy"><img src="https://avatars.githubusercontent.com/u/1290739?v=4" title="thomasplevy" width="80" height="80"></a> -<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> -<a href="https://github.com/therealmarknelson"><img src="https://avatars.githubusercontent.com/u/5050601?v=4" title="therealmarknelson" width="80" height="80"></a> -<a href="https://github.com/PSmolic"><img src="https://avatars.githubusercontent.com/u/4542049?v=4" title="PSmolic" width="80" height="80"></a> -<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> -<a href="https://github.com/pondermatic"><img src="https://avatars.githubusercontent.com/u/5377968?v=4" title="pondermatic" width="80" height="80"></a> -<a href="https://github.com/bmatt468"><img src="https://avatars.githubusercontent.com/u/8673706?v=4" title="bmatt468" width="80" height="80"></a> -<a href="https://github.com/chrisbadgett"><img src="https://avatars.githubusercontent.com/u/12163552?v=4" title="chrisbadgett" width="80" height="80"></a> -<a href="https://github.com/MaximilianoRicoTabo"><img src="https://avatars.githubusercontent.com/u/1678457?v=4" title="MaximilianoRicoTabo" width="80" height="80"></a> -<a href="https://github.com/alimathis"><img src="https://avatars.githubusercontent.com/u/16086976?v=4" title="alimathis" width="80" height="80"></a> -<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> -<a href="https://github.com/andreasblumberg"><img src="https://avatars.githubusercontent.com/u/1697968?v=4" title="andreasblumberg" width="80" height="80"></a> -<a href="https://github.com/philwp"><img src="https://avatars.githubusercontent.com/u/5949352?v=4" title="philwp" width="80" height="80"></a> -<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> -<a href="https://github.com/chetansatasiya"><img src="https://avatars.githubusercontent.com/u/7081284?v=4" title="chetansatasiya" width="80" height="80"></a> -<a href="https://github.com/imknight"><img src="https://avatars.githubusercontent.com/u/77604?v=4" title="imknight" width="80" height="80"></a> -<a href="https://github.com/Mte90"><img src="https://avatars.githubusercontent.com/u/403283?v=4" title="Mte90" width="80" height="80"></a> -<a href="https://github.com/jdevalk"><img src="https://avatars.githubusercontent.com/u/487629?v=4" title="jdevalk" width="80" height="80"></a> -<a href="https://github.com/nikolapasic"><img src="https://avatars.githubusercontent.com/u/10199798?v=4" title="nikolapasic" width="80" height="80"></a> -<a href="https://github.com/AndreaBarghigiani"><img src="https://avatars.githubusercontent.com/u/190159?v=4" title="AndreaBarghigiani" width="80" height="80"></a> -<a href="https://github.com/yojance"><img src="https://avatars.githubusercontent.com/u/1916064?v=4" title="yojance" width="80" height="80"></a> -<a href="https://github.com/tpkemme"><img src="https://avatars.githubusercontent.com/u/3424234?v=4" title="tpkemme" width="80" height="80"></a> -<a href="https://github.com/dineshchouhan"><img src="https://avatars.githubusercontent.com/u/15683967?v=4" title="dineshchouhan" width="80" height="80"></a> -<a href="https://github.com/mcguffin"><img src="https://avatars.githubusercontent.com/u/402988?v=4" title="mcguffin" width="80" height="80"></a> -<a href="https://github.com/wenchen"><img src="https://avatars.githubusercontent.com/u/959457?v=4" title="wenchen" width="80" height="80"></a> -<a href="https://github.com/paulgoodchild"><img src="https://avatars.githubusercontent.com/u/10562196?v=4" title="paulgoodchild" width="80" height="80"></a> -<a href="https://github.com/yumashev"><img src="https://avatars.githubusercontent.com/u/37841388?v=4" title="yumashev" width="80" height="80"></a> -<a href="https://github.com/jasonyingling"><img src="https://avatars.githubusercontent.com/u/4986487?v=4" title="jasonyingling" width="80" height="80"></a> -<a href="https://github.com/mrosati84"><img src="https://avatars.githubusercontent.com/u/855068?v=4" title="mrosati84" width="80" height="80"></a> -<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> -<a href="https://github.com/ThePikJoker"><img src="https://avatars.githubusercontent.com/u/16877156?v=4" title="ThePikJoker" width="80" height="80"></a> -<a href="https://github.com/tnorthcutt"><img src="https://avatars.githubusercontent.com/u/796639?v=4" title="tnorthcutt" width="80" height="80"></a> -<a href="https://github.com/hovpoghosyan"><img src="https://avatars.githubusercontent.com/u/9405480?v=4" title="hovpoghosyan" width="80" height="80"></a> -<a href="https://github.com/nrherron92"><img src="https://avatars.githubusercontent.com/u/47434271?v=4" title="nrherron92" width="80" height="80"></a> -<a href="https://github.com/README1ST"><img src="https://avatars.githubusercontent.com/u/30046495?v=4" title="README1ST" width="80" height="80"></a> -<a href="https://github.com/andrewvaughan"><img src="https://avatars.githubusercontent.com/u/1119590?v=4" title="andrewvaughan" width="80" height="80"></a> -<a href="https://github.com/CadenG150"><img src="https://avatars.githubusercontent.com/u/30481164?v=4" title="CadenG150" width="80" height="80"></a> -<a href="https://github.com/unt01d"><img src="https://avatars.githubusercontent.com/u/11303423?v=4" title="unt01d" width="80" height="80"></a> -<a href="https://github.com/iTechsTR"><img src="https://avatars.githubusercontent.com/u/33372714?v=4" title="iTechsTR" width="80" height="80"></a> -<a href="https://github.com/moorscode"><img src="https://avatars.githubusercontent.com/u/2005352?v=4" title="moorscode" width="80" height="80"></a> -<a href="https://github.com/nhandl3"><img src="https://avatars.githubusercontent.com/u/1247539?v=4" title="nhandl3" width="80" height="80"></a> -<a href="https://github.com/Nikschavan"><img src="https://avatars.githubusercontent.com/u/2931091?v=4" title="Nikschavan" width="80" height="80"></a> -<a href="https://github.com/reedhewitt"><img src="https://avatars.githubusercontent.com/u/957141?v=4" title="reedhewitt" width="80" height="80"></a> -<a href="https://github.com/edent"><img src="https://avatars.githubusercontent.com/u/837136?v=4" title="edent" width="80" height="80"></a> -<a href="https://github.com/sujaypawar"><img src="https://avatars.githubusercontent.com/u/2222249?v=4" title="sujaypawar" width="80" height="80"></a> - -[//]: contributor-faces - - -### Partners and Sponsors - -[<img src="https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/.github/sponsors/browserstack-logo.png" height="60" alt="BrowserStack">](https://www.browserstack.com/) - -[BrowserStack](https://www.browserstack.com/) helps us ensure LifterLMS looks great and works on every imaginable browser and device. - -<!-- References: Links --> -[link-cc]: https://codeclimate.com/github/gocodebox/lifterlms "LifterLMS on Code Climate" -[link-cc-coverage]: https://codeclimate.com/github/gocodebox/lifterlms/coverage "Code coverage reports on Code Climate" -[link-experts]: https://lifterlms.com/docs/do-you-have-any-recommended-developers-who-can-modifycustomize-lifterlms/ "Hire a LifterLMS Expert" -[link-php]: https://www.php.net/supported-versions "PHP Support Versions" -[link-phpunit-tests]: https://github.com/gocodebox/lifterlms/actions/workflows/test-phpunit.yml "PHPUnit Tests Status" -[link-phpcs-checks]: https://github.com/gocodebox/lifterlms/actions/workflows/coding-standards.yml "PHPCS Coding Standards Checks" -[link-slack]: https://lifterlms.com/slack "Chat with the community on Slack" -[link-support]: https://lifterlms.com/my-account/my-tickets "LifterLMS customer support" -[link-support-forums]: https://wordpress.org/support/plugin/lifterlms "LifterLMS user support forums" -[link-wp-advanced]:https://wordpress.org/plugins/lifterlms/advanced/ "Advanced plugin details on the WordPress plugin repository" -[link-wp-repo]:https://wordpress.org/plugins/lifterlms/ "LifterLMS on the WordPress plugin repository" -[link-wp-reviews]:https://wordpress.org/support/plugin/lifterlms/reviews/ "Leave a review on the WordPress plugin repository" - -[img-cc-coverage]:https://img.shields.io/codeclimate/coverage/gocodebox/lifterlms?style=for-the-badge&logo=code-climate -[img-cc-maintainability]:https://img.shields.io/codeclimate/maintainability/gocodebox/lifterlms?logo=code-climate&style=for-the-badge -[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 -[img-contributions-welcome]: https://img.shields.io/badge/contributions-welcome-blue.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik02NzIgMTQ3MnEwLTQwLTI4LTY4dC02OC0yOC02OCAyOC0yOCA2OCAyOCA2OCA2OCAyOCA2OC0yOCAyOC02OHptMC0xMTUycTAtNDAtMjgtNjh0LTY4LTI4LTY4IDI4LTI4IDY4IDI4IDY4IDY4IDI4IDY4LTI4IDI4LTY4em02NDAgMTI4cTAtNDAtMjgtNjh0LTY4LTI4LTY4IDI4LTI4IDY4IDI4IDY4IDY4IDI4IDY4LTI4IDI4LTY4em05NiAwcTAgNTItMjYgOTYuNXQtNzAgNjkuNXEtMiAyODctMjI2IDQxNC02NyAzOC0yMDMgODEtMTI4IDQwLTE2OS41IDcxdC00MS41IDEwMHYyNnE0NCAyNSA3MCA2OS41dDI2IDk2LjVxMCA4MC01NiAxMzZ0LTEzNiA1Ni0xMzYtNTYtNTYtMTM2cTAtNTIgMjYtOTYuNXQ3MC02OS41di04MjBxLTQ0LTI1LTcwLTY5LjV0LTI2LTk2LjVxMC04MCA1Ni0xMzZ0MTM2LTU2IDEzNiA1NiA1NiAxMzZxMCA1Mi0yNiA5Ni41dC03MCA2OS41djQ5N3E1NC0yNiAxNTQtNTcgNTUtMTcgODcuNS0yOS41dDcwLjUtMzEgNTktMzkuNSA0MC41LTUxIDI4LTY5LjUgOC41LTkxLjVxLTQ0LTI1LTcwLTY5LjV0LTI2LTk2LjVxMC04MCA1Ni0xMzZ0MTM2LTU2IDEzNiA1NiA1NiAxMzZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+ -[img-php]: https://img.shields.io/badge/PHP-7.2%2B-brightgreen?style=for-the-badge&logoColor=white&logo=php -[img-phpunit-tests]: https://img.shields.io/github/workflow/status/gocodebox/lifterlms/Test%20PHPUnit?label=PHPUnit&logo=github&style=for-the-badge -[img-phpcs-checks]: https://img.shields.io/github/workflow/status/gocodebox/lifterlms/Coding%20Standards?label=PHPCS&logo=github&style=for-the-badge -[img-slack]: https://img.shields.io/badge/chat-on%20slack-blueviolet?style=for-the-badge&logo=slack -[img-wp-downloads]: https://img.shields.io/wordpress/plugin/dt/lifterlms.svg?style=for-the-badge&logo=wordpress -[img-wp-installs]: https://img.shields.io/wordpress/plugin/installs/lifterlms.svg?style=for-the-badge&logo=wordpress -[img-wp-plugin]:https://img.shields.io/wordpress/plugin/v/lifterlms.svg?style=for-the-badge&logo=wordpress -[img-wp-rating]:https://img.shields.io/wordpress/plugin/r/lifterlms.svg?style=for-the-badge&logo=wordpress -[img-wp-tested]:https://img.shields.io/wordpress/v/lifterlms.svg?style=for-the-badge&logo=wordpress diff --git a/_private/svg/llms-icon-calendar.svg b/_private/svg/llms-icon-calendar.svg deleted file mode 100644 index 4e02ca4d61..0000000000 --- a/_private/svg/llms-icon-calendar.svg +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="101.802px" height="100px" viewBox="0 0 101.802 100" enable-background="new 0 0 101.802 100" xml:space="preserve"> -<g> - <g id="_x31_5_38_"> - <g> - <path d="M24.876,23.083h1.457c2.214,0,4.008-1.777,4.008-3.968V8.163V4.067c0-2.192-1.794-3.968-4.008-3.968h-1.457 - c-2.213,0-4.007,1.776-4.007,3.968v4.096v10.952C20.869,21.306,22.663,23.083,24.876,23.083z"/> - <path d="M76.28,22.985h1.457c2.212,0,4.006-1.777,4.006-3.969V7.124V3.968c0-2.19-1.794-3.968-4.006-3.968H76.28 - c-2.214,0-4.01,1.777-4.01,3.968v3.156v11.893C72.272,21.208,74.066,22.985,76.28,22.985z"/> - <path d="M95.287,7.972H85v12.044c0,4.019-3.26,6.235-7.264,6.235h-1.455c-4.007,0-7.268-3.269-7.268-7.287V7.972H33.599v11.091 - c0,4.019-3.259,7.287-7.266,7.287h-1.456c-4.005,0-7.265-3.269-7.265-7.287V7.972H6.515C2.922,7.972,0,10.903,0,14.506v78.959 - C0,97.069,2.922,100,6.515,100h88.771c3.592,0,6.516-2.931,6.516-6.535V14.506C101.802,10.903,98.878,7.972,95.287,7.972z - M95.287,93.465H6.516l0-59.628h88.773l0.002,59.628C95.291,93.465,95.289,93.465,95.287,93.465z"/> - <path d="M54.264,53.84h11.698c0.462,0,0.839-0.38,0.839-0.843v-10.16c0-0.465-0.377-0.842-0.839-0.842H54.264 - c-0.464,0-0.839,0.376-0.839,0.842v10.16C53.425,53.46,53.8,53.84,54.264,53.84z"/> - <path d="M73.355,53.84h11.698c0.463,0,0.839-0.38,0.839-0.843v-10.16c0-0.465-0.376-0.842-0.839-0.842H73.355 - c-0.464,0-0.839,0.376-0.839,0.842v10.16C72.516,53.46,72.891,53.84,73.355,53.84z"/> - <path d="M16.082,70.472H27.78c0.463,0,0.839-0.377,0.839-0.842V59.47c0-0.467-0.376-0.843-0.839-0.843H16.082 - c-0.463,0-0.839,0.376-0.839,0.843v10.16C15.242,70.095,15.619,70.472,16.082,70.472z"/> - <path d="M35.173,70.472h11.697c0.464,0,0.839-0.377,0.839-0.842V59.47c0-0.467-0.375-0.843-0.839-0.843H35.173 - c-0.464,0-0.84,0.376-0.84,0.843v10.16C34.333,70.095,34.709,70.472,35.173,70.472z"/> - <path fill="#777676" d="M54.264,70.472h11.698c0.462,0,0.839-0.377,0.839-0.842V59.47c0-0.467-0.377-0.843-0.839-0.843H54.264 - c-0.462,0-0.839,0.376-0.839,0.843v10.16C53.425,70.095,53.802,70.472,54.264,70.472z"/> - <path d="M73.355,70.472h11.698c0.463,0,0.839-0.377,0.839-0.842V59.47c0-0.467-0.376-0.843-0.839-0.843H73.355 - c-0.464,0-0.839,0.376-0.839,0.843v10.16C72.516,70.095,72.891,70.472,73.355,70.472z"/> - <path d="M27.78,75.26H16.082c-0.463,0-0.839,0.376-0.839,0.843v10.16c0,0.465,0.376,0.841,0.839,0.841H27.78 - c0.463,0,0.839-0.376,0.839-0.841v-10.16C28.62,75.636,28.244,75.26,27.78,75.26z"/> - <path d="M46.871,75.26H35.174c-0.465,0-0.841,0.376-0.841,0.843v10.16c0,0.465,0.376,0.841,0.841,0.841h11.696 - c0.464,0,0.839-0.376,0.839-0.841v-10.16C47.71,75.636,47.335,75.26,46.871,75.26z"/> - <path d="M65.962,75.26H54.264c-0.462,0-0.839,0.376-0.839,0.843v10.16c0,0.465,0.377,0.841,0.839,0.841h11.698 - c0.464,0,0.839-0.376,0.839-0.841v-10.16C66.801,75.636,66.426,75.26,65.962,75.26z"/> - <path d="M85.053,75.26H73.355c-0.464,0-0.839,0.376-0.839,0.843v10.16c0,0.465,0.375,0.841,0.839,0.841h11.698 - c0.463,0,0.839-0.376,0.839-0.841v-10.16C85.892,75.636,85.516,75.26,85.053,75.26z"/> - </g> - </g> -</g> -</svg> diff --git a/_private/svg/llms-icon-checkmark.svg b/_private/svg/llms-icon-checkmark.svg deleted file mode 100644 index c8bdd2eb0c..0000000000 --- a/_private/svg/llms-icon-checkmark.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="99.235px" height="79.441px" viewBox="0 0 99.235 79.441" enable-background="new 0 0 99.235 79.441" xml:space="preserve"> -<g> - <path d="M23.925,75.434c2.565,2.564,6.011,4.008,9.618,4.008c3.606,0,7.053-1.443,9.618-4.008l51.938-51.938 - c5.208-5.21,5.609-13.786,0.562-19.157c-5.291-5.69-14.188-5.771-19.558-0.321L39.234,40.888c-3.126,3.126-8.176,3.126-11.302,0 - l-4.729-4.809c-5.29-5.289-13.947-5.289-19.236,0c-5.29,5.291-5.29,13.946,0,19.235L23.925,75.434z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-circle-empty.svg b/_private/svg/llms-icon-circle-empty.svg deleted file mode 100644 index cff0485a3a..0000000000 --- a/_private/svg/llms-icon-circle-empty.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="99.999px" viewBox="0 0 100 99.999" enable-background="new 0 0 100 99.999" xml:space="preserve"> -<g> - <path d="M50,0C22.387,0,0,22.387,0,50c0,27.611,22.387,50,50,50c27.61,0,50-22.389,50-50C100,22.387,77.61,0,50,0z M49.886,89.521 - c-22.092,0-40-17.91-40-40c0-22.092,17.908-40,40-40c22.09,0,40,17.908,40,40C89.886,71.61,71.976,89.521,49.886,89.521z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-circle.svg b/_private/svg/llms-icon-circle.svg deleted file mode 100644 index 37eafb0f77..0000000000 --- a/_private/svg/llms-icon-circle.svg +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> -<path d="M100,50c0,27.611-22.389,50-50,50C22.388,100,0,77.611,0,50C0,22.388,22.388,0,50,0C77.611,0,100,22.388,100,50z"/> -</svg> diff --git a/_private/svg/llms-icon-close.svg b/_private/svg/llms-icon-close.svg deleted file mode 100644 index 08ede2e8ea..0000000000 --- a/_private/svg/llms-icon-close.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="41.347px" height="41.347px" viewBox="0 0 41.347 41.347" enable-background="new 0 0 41.347 41.347" xml:space="preserve"> -<path d="M39.552,32.456L27.769,20.673L39.552,8.89c2.189-2.189,2.405-5.524,0.481-7.448l-0.129-0.129 - c-1.923-1.924-5.259-1.708-7.448,0.482L20.673,13.578L8.89,1.794C6.701-0.395,3.366-0.611,1.442,1.313L1.313,1.442 - C-0.611,3.365-0.395,6.701,1.795,8.89l11.783,11.783L1.795,32.456c-2.19,2.19-2.406,5.526-0.482,7.448l0.129,0.129 - c1.924,1.924,5.258,1.709,7.448-0.481l11.783-11.783l11.783,11.783c2.19,2.19,5.526,2.406,7.448,0.482l0.129-0.13 - C41.957,37.98,41.742,34.646,39.552,32.456z"/> -</svg> diff --git a/_private/svg/llms-icon-course-section.svg b/_private/svg/llms-icon-course-section.svg deleted file mode 100644 index c4fa2f9590..0000000000 --- a/_private/svg/llms-icon-course-section.svg +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="74.075px" height="59.345px" viewBox="14.259 4.771 74.075 59.345" enable-background="new 14.259 4.771 74.075 59.345" - xml:space="preserve"> -<g id="Captions"> -</g> -<g id="Your_Icon"> - <path d="M85.367,46.827c-1.463-1.462-3.26-2.382-5.146-2.763V30.686v-0.018c0.008-0.399-0.119-0.778-0.391-1.052l-0.014-0.015 - l0.01-0.011l-3.621-2.077L61.25,18.928v-0.002l-0.045-0.045c-0.176-0.173-0.39-0.288-0.626-0.345 - c1.392-3.621,0.633-7.878-2.29-10.797c-3.954-3.956-10.366-3.956-14.322,0c-2.953,2.954-3.699,7.276-2.242,10.925 - c-0.112,0.059-0.22,0.134-0.312,0.227l-0.041,0.042l0,0l-14.97,8.593l-3.61,2.07l-0.009-0.011l-0.019,0.02 - c-0.218,0.218-0.34,0.505-0.379,0.813c-0.017,0.141-0.017,0.291,0,0.443v13.199c-1.893,0.382-3.693,1.302-5.161,2.767 - c-3.956,3.955-3.954,10.367,0,14.323c3.957,3.956,10.369,3.956,14.323,0s3.954-10.368,0-14.323 - c-1.463-1.462-3.261-2.382-5.146-2.763V31.998l17.476-10.032c0.03,0.031,0.059,0.064,0.089,0.096 - c1.467,1.466,3.268,2.387,5.163,2.768l-0.005,19.231c-1.89,0.38-3.69,1.3-5.158,2.766c-3.954,3.955-3.954,10.367,0,14.323 - c3.957,3.956,10.368,3.956,14.322,0c3.957-3.956,3.957-10.368,0-14.323c-1.46-1.462-3.261-2.382-5.145-2.763V24.824 - c1.887-0.381,3.686-1.299,5.145-2.762c0.078-0.078,0.15-0.158,0.227-0.235l17.69,10.153v12.08 - c-1.893,0.382-3.694,1.302-5.159,2.767c-3.955,3.955-3.955,10.367,0,14.323c3.954,3.956,10.367,3.956,14.321,0 - S89.322,50.781,85.367,46.827z"/> -</g> -</svg> \ No newline at end of file diff --git a/_private/svg/llms-icon-existing-lesson.svg b/_private/svg/llms-icon-existing-lesson.svg deleted file mode 100644 index c81a2e1444..0000000000 --- a/_private/svg/llms-icon-existing-lesson.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="57.167px" height="67.093px" viewBox="25.833 5.809 57.167 67.093" enable-background="new 25.833 5.809 57.167 67.093" - xml:space="preserve"> -<g> - <polygon fill="none" points="76.76,30.078 76.76,59.559 78.25,59.559 78.25,10.309 40.25,10.309 40.25,12.717 59.398,12.717 "/> - <polygon fill="none" points="53.611,35.865 53.611,17.346 40.25,17.346 40.25,59.559 72.129,59.559 72.129,35.865 "/> - <polygon fill="none" points="35.25,17.346 30.462,17.346 30.462,68.271 72.129,68.271 72.129,63.809 35.25,63.809 "/> - <polygon fill="none" points="58.24,18.504 58.24,31.235 70.973,31.235 "/> - <path d="M35.25,5.809v6.908h-9.417v60.186H76.76v-9.094H83v-58H35.25z M72.129,68.271H30.462V17.346h4.787h5h13.362v18.519h18.518 - v23.694v4.25V68.271z M58.24,31.235V18.504l12.732,12.731H58.24z M78.25,59.559h-1.49V30.078L59.398,12.717H40.25v-2.408h38V59.559 - z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-facebook.svg b/_private/svg/llms-icon-facebook.svg deleted file mode 100644 index 052d7607ae..0000000000 --- a/_private/svg/llms-icon-facebook.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.421px" height="96.417px" viewBox="0 0 96.421 96.417" enable-background="new 0 0 96.421 96.417" xml:space="preserve"> -<path fill="#FFFFFF" d="M96.421,48.208c0,26.628-21.586,48.209-48.212,48.209C21.582,96.417,0,74.836,0,48.208 - C0,21.581,21.582,0,48.209,0C74.835,0,96.421,21.581,96.421,48.208z"/> -<path fill="#41A7DE" d="M48.178,5.217c-23.666,0-42.855,19.19-42.855,42.854c0,23.67,19.19,42.854,42.855,42.854 - c23.668,0,42.853-19.185,42.853-42.854C91.031,24.407,71.846,5.217,48.178,5.217z M57.359,48.088h-6.007c0,9.594,0,21.41,0,21.41 - h-8.898c0,0,0-11.7,0-21.41h-4.233v-7.566h4.233v-4.898c0-3.505,1.664-8.979,8.978-8.979l6.595,0.027v7.346c0,0-4.008,0-4.785,0 - c-0.779,0-1.89,0.385-1.89,2.058v4.447h6.786L57.359,48.088z"/> -</svg> diff --git a/_private/svg/llms-icon-folder.svg b/_private/svg/llms-icon-folder.svg deleted file mode 100644 index 78a73d7c3a..0000000000 --- a/_private/svg/llms-icon-folder.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.372px" height="82.271px" viewBox="0 0 98.372 82.271" enable-background="new 0 0 98.372 82.271" xml:space="preserve"> -<path d="M97.278,28.066c-1.18-1.663-3.158-2.612-5.439-2.612H83.17v-9.43c0-3.629-2.957-6.588-6.588-6.588H46.187 - c-1.294,0-3.311-1.023-4.068-2.068l-2.277-3.11C38.068,1.826,34.464,0,31.456,0H20.92c-3.309,0-6.637,2.35-7.739,5.471l-0.725,2.054 - c-0.344,0.966-1.681,1.912-2.707,1.912H6.595C2.956,9.437,0,12.396,0,16.024v59.513c0,0.318,0.061,0.621,0.159,0.896 - c0.057,1.182,0.418,2.295,1.091,3.23c1.172,1.654,3.154,2.605,5.43,2.605h67.143c4.062,0,8.285-2.998,9.629-6.826l14.492-41.396 - C98.694,31.904,98.45,29.719,97.278,28.066z M6.595,14.784h3.154c3.311,0,6.639-2.355,7.741-5.484l0.724-2.043 - c0.345-0.965,1.685-1.912,2.706-1.912h10.536c1.292,0,3.309,1.026,4.072,2.068l2.271,3.11c1.775,2.425,5.384,4.261,8.388,4.261 - h30.396c0.676,0,1.25,0.568,1.25,1.24v9.43H24.701c-4.056,0-8.289,2.999-9.628,6.837L5.345,60.062V16.024 - C5.345,15.353,5.913,14.784,6.595,14.784z M92.901,32.291L78.409,73.676c-0.594,1.695-2.783,3.25-4.586,3.25H6.68 - c-0.516,0-0.917-0.131-1.077-0.354c-0.155-0.227-0.146-0.646,0.023-1.129l14.489-41.396c0.6-1.697,2.78-3.249,4.586-3.249h67.137 - c0.518,0,0.924,0.13,1.08,0.354C93.077,31.376,93.069,31.799,92.901,32.291z"/> -</svg> diff --git a/_private/svg/llms-icon-free.svg b/_private/svg/llms-icon-free.svg deleted file mode 100644 index 97f8e04613..0000000000 --- a/_private/svg/llms-icon-free.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="210px" height="100px" viewBox="0 0 210 100" enable-background="new 0 0 210 100" xml:space="preserve"> -<g> - <path d="M92.284,43.255c0.421-0.912,0.633-2.071,0.633-3.475c0-2.106-0.599-4.065-1.792-5.47S88.213,32,85.965,32H75v16h10.965 c0.796,0,1.603-0.693,2.423-0.881c0.818-0.187,1.567-0.849,2.246-1.389C91.312,45.194,91.862,44.168,92.284,43.255z"/> - <path d="M210,14.19C210,6.353,203.646,0,195.81,0H14.19C6.354,0,0,6.353,0,14.19v71.62C0,93.647,6.354,100,14.19,100H195.81 c7.837,0,14.19-6.353,14.19-14.19V14.19z M53.604,46c1.029,0,1.896,0.073,2.597,0.752c0.703,0.678,1.054,1.399,1.054,2.429 c0,0.982-0.351,1.468-1.054,2.146C55.5,52.006,54.634,52,53.604,52H37v19.581c0,1.029-0.238,1.895-0.963,2.597 c-0.729,0.703-1.519,1.053-2.502,1.053c-1.028,0-1.792-0.351-2.539-1.053C30.247,73.476,30,72.609,30,71.581V29.812 c0-1.217,0.348-2.43,1.237-3.319C32.125,25.605,33.107,25,34.371,25h20.147c1.028,0,1.895,0.5,2.597,1.177 c0.702,0.68,1.053,1.615,1.053,2.643c0,0.983-0.351,1.65-1.053,2.329C56.412,31.826,55.546,32,54.518,32H37v14H53.604z M99.479,74.143c-0.726,0.725-1.579,1.088-2.562,1.088c-0.656,0-1.254-0.152-1.791-0.456c-0.537-0.305-0.972-0.683-1.299-1.245 L82.104,54H75v17.581c0,1.029,0.02,1.895-0.729,2.597c-0.748,0.703-1.418,1.053-2.4,1.053c-1.03,0-1.983-0.351-2.709-1.053 C68.437,73.476,68,72.609,68,71.581V29.812c0-1.217,0.619-2.43,1.507-3.319C70.396,25.605,71.646,25,72.908,25h13.057 c4.258,0,7.698,1.47,10.319,4.091c2.713,2.715,4.071,6.304,4.071,10.61c0,4.306-1.333,7.786-4,10.358 c-1.64,1.545-3.675,2.666-6.107,3.32l9.757,16.366c0.374,0.608,0.561,1.246,0.561,1.9C100.565,72.583,100.204,73.418,99.479,74.143 z M139.63,73.746c-0.725,0.679-1.579,1.254-2.562,1.254H116.22c-1.218,0-2.109-0.668-3.024-1.533 c-0.91-0.866-1.195-2.167-1.195-3.43V29.812c0-1.217,0.273-2.43,1.163-3.319c0.888-0.888,1.795-1.493,3.057-1.493h20.357 c1.029,0,1.896,0.511,2.598,1.212c0.702,0.702,1.054,1.649,1.054,2.678c0,0.983-0.352,1.639-1.054,2.292 c-0.701,0.657-1.568,0.818-2.598,0.818H119v14h16.665c1.027,0,1.896,0.073,2.597,0.752c0.701,0.678,1.053,1.399,1.053,2.429 c0,0.982-0.352,1.468-1.053,2.146c-0.701,0.679-1.569,0.673-2.597,0.673H119v15h18.068c0.982,0,1.837,0.56,2.562,1.237 c0.727,0.679,1.089,1.736,1.089,2.721C140.719,71.987,140.356,73.068,139.63,73.746z M180.135,73.746 c-0.726,0.679-1.578,1.254-2.562,1.254h-20.85c-1.217,0-1.86-0.668-2.774-1.533C153.037,72.601,153,71.3,153,70.037V29.812 c0-1.217,0.025-2.43,0.916-3.319c0.888-0.888,1.544-1.493,2.808-1.493h20.357c1.029,0,1.896,0.511,2.598,1.212 c0.702,0.702,1.053,1.649,1.053,2.678c0,0.983-0.351,1.639-1.053,2.292C178.977,31.839,178.11,32,177.081,32H160v14h16.168 c1.029,0,1.896,0.073,2.598,0.752c0.702,0.678,1.054,1.399,1.054,2.429c0,0.982-0.352,1.468-1.054,2.146 c-0.702,0.679-1.568,0.673-2.598,0.673H160v15h17.573c0.983,0,1.836,0.56,2.562,1.237c0.726,0.679,1.088,1.736,1.088,2.721 C181.223,71.987,180.86,73.068,180.135,73.746z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-gear.svg b/_private/svg/llms-icon-gear.svg deleted file mode 100644 index f5e213cc24..0000000000 --- a/_private/svg/llms-icon-gear.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> -<g> - <g id="cog"> - <path d="M100,56.153v-12.5l-14.94-6.225c-0.416-1.172-0.854-2.306-1.391-3.419L89.7,18.994l-8.841-8.837 - l-14.881,6.128c-1.135-0.55-2.294-1.012-3.491-1.44L56.153,0h-12.5l-6.178,14.794c-1.244,0.44-2.441,0.903-3.625,1.465 - l-14.856-5.969l-8.837,8.837l6.056,14.722c-0.587,1.209-1.062,2.44-1.516,3.712L0,43.847v12.5l14.709,6.128 - c0.453,1.27,0.94,2.504,1.525,3.713l-5.944,14.818l8.837,8.838l14.76-6.078c1.184,0.562,2.394,1.013,3.637,1.44L43.847,100h12.5 - l6.188-14.869c1.185-0.44,2.356-0.902,3.479-1.44l14.99,6.007l8.838-8.838l-6.153-14.916c0.525-1.121,0.953-2.259,1.369-3.418 - L100,56.153z M49.903,68.75c-10.35,0-18.75-8.4-18.75-18.75c0-10.35,8.4-18.75,18.75-18.75c10.35,0,18.75,8.4,18.75,18.75 - C68.653,60.35,60.253,68.75,49.903,68.75z"/> - </g> -</g> -</svg> diff --git a/_private/svg/llms-icon-google.svg b/_private/svg/llms-icon-google.svg deleted file mode 100644 index e5ae714900..0000000000 --- a/_private/svg/llms-icon-google.svg +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.42px" height="96.418px" viewBox="0 0 96.42 96.418" enable-background="new 0 0 96.42 96.418" xml:space="preserve"> -<path fill="#FFFFFF" d="M96.42,48.209c0,26.627-21.584,48.209-48.213,48.209C21.582,96.418,0,74.836,0,48.209S21.582,0,48.207,0 - C74.836,0,96.42,21.582,96.42,48.209z"/> -<path fill="#449ED8" d="M39.4,29.062c-3.396-0.104-5.682,3.321-5.092,7.787c0.588,4.474,3.818,7.599,7.225,7.701 - c3.402,0.099,5.361-2.777,4.775-7.25C45.725,32.833,42.807,29.166,39.4,29.062z"/> -<path fill="#449ED8" d="M40.836,53.701c-5.074-0.059-9.371,3.201-9.371,6.981c0,3.856,3.666,7.066,8.732,7.066 - c7.137,0,9.621-3.011,9.621-6.871c0-0.466-0.055-0.923-0.162-1.361c-0.561-2.181-2.777-3.38-5.535-5.296 - C43.123,53.898,42.018,53.711,40.836,53.701z"/> -<path fill="#41A7DE" d="M48.109,5.578c-23.666,0-42.854,19.188-42.854,42.854c0,23.672,19.188,42.855,42.854,42.855 - s42.854-19.184,42.854-42.855C90.963,24.767,71.775,5.578,48.109,5.578z M51.381,37.111c0,2.793-1.547,5.045-3.727,6.746 - c-2.137,1.67-2.537,2.368-2.537,3.785c0,1.209,2.547,3.006,3.719,3.889c4.074,3.057,4.902,4.981,4.902,8.809 - c0,4.771-5.141,9.518-13.508,9.518c-7.34,0-13.533-2.98-13.533-7.758c0-4.844,5.137-9.895,12.475-9.895 - c0.799,0,1.533-0.023,2.293-0.023c-1.004-0.97-1.816-1.815-1.816-3.288c0-0.882,0.277-1.718,0.67-2.464 - c-0.398,0.023-0.805,0.051-1.223,0.051c-6.033,0-9.543-4.239-9.543-9.549c0-5.196,5.348-9.926,11.756-9.926 - c3.303,0,12.627,0,12.627,0l-2.818,2.965h-3.318C50.137,31.31,51.381,34.067,51.381,37.111z M69.52,36.332h-5.828v5.824h-2.914 - v-5.824h-5.826v-2.914h5.826V27.59h2.914v5.828h5.828V36.332z"/> -</svg> diff --git a/_private/svg/llms-icon-graph.svg b/_private/svg/llms-icon-graph.svg deleted file mode 100644 index d44b4b3562..0000000000 --- a/_private/svg/llms-icon-graph.svg +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.976px" height="81.71px" viewBox="0 0 98.976 81.71" enable-background="new 0 0 98.976 81.71" xml:space="preserve"> -<g> - <path d="M5.822,56.414H2.811c-0.833,0-1.505,0.673-1.505,1.506v22.285c0,0.828,0.672,1.505,1.505,1.505h3.011 - c0.833,0,1.505-0.677,1.505-1.505V57.92C7.327,57.087,6.655,56.414,5.822,56.414z"/> - <path d="M23.891,56.414H20.88c-0.831,0-1.505,0.673-1.505,1.506v22.285c0,0.828,0.674,1.505,1.505,1.505h3.011 - c0.831,0,1.505-0.677,1.505-1.505V57.92C25.396,57.087,24.721,56.414,23.891,56.414z"/> - <path d="M41.959,40.15h-3.011c-0.831,0-1.505,0.677-1.505,1.51v38.545c0,0.828,0.675,1.505,1.505,1.505h3.011 - c0.833,0,1.505-0.677,1.505-1.505V41.66C43.464,40.827,42.792,40.15,41.959,40.15z"/> - <path d="M60.028,27.504h-3.011c-0.831,0-1.505,0.673-1.505,1.506v51.195c0,0.828,0.675,1.505,1.505,1.505h3.011 - c0.831,0,1.508-0.677,1.508-1.505V29.01C61.536,28.177,60.859,27.504,60.028,27.504z"/> - <path d="M78.098,40.15h-3.013c-0.83,0-1.505,0.677-1.505,1.51v38.545c0,0.828,0.675,1.505,1.505,1.505h3.013 - c0.831,0,1.504-0.677,1.504-1.505V41.66C79.602,40.827,78.929,40.15,78.098,40.15z"/> - <path d="M96.166,20.276h-3.011c-0.831,0-1.504,0.673-1.504,1.506v58.423c0,0.828,0.673,1.505,1.504,1.505h3.011 - c0.83,0,1.505-0.677,1.505-1.505V21.782C97.67,20.949,96.996,20.276,96.166,20.276z"/> - <path d="M94.66,0c-2.38,0-4.314,1.935-4.314,4.312c0,0.915,0.284,1.761,0.768,2.455L77.927,25.669 - c-0.419-0.139-0.87-0.217-1.334-0.217c-1.023,0-1.963,0.355-2.7,0.954l-11.169-8.068c0.071-0.312,0.115-0.643,0.115-0.98 - c0-2.382-1.938-4.316-4.317-4.316c-2.379,0-4.314,1.935-4.314,4.316c0,0.391,0.057,0.771,0.154,1.137l-12.102,9.413 - c-0.551-0.26-1.161-0.403-1.807-0.403c-2.377,0-4.312,1.931-4.316,4.309L25.24,37.625c-0.761-0.677-1.759-1.084-2.854-1.084 - c-1.614,0-3.021,0.885-3.764,2.203L8.544,37.179c-0.399-1.966-2.145-3.449-4.228-3.449C1.937,33.729,0,35.664,0,38.042 - c0,2.382,1.937,4.32,4.316,4.32c1.614,0,3.021-0.894,3.764-2.212l10.075,1.57c0.404,1.97,2.146,3.453,4.23,3.453 - c2.379,0,4.316-1.939,4.316-4.321c0-0.19-0.018-0.382-0.043-0.572l10.548-5.622c0.794,0.902,1.952,1.475,3.247,1.475 - c2.38,0,4.317-1.935,4.317-4.312c0-0.624-0.137-1.223-0.38-1.761l11.729-9.119c0.69,0.46,1.514,0.729,2.403,0.729 - c0.944,0,1.816-0.304,2.527-0.819l11.297,8.16c-0.043,0.243-0.071,0.499-0.071,0.759c0,2.382,1.935,4.316,4.316,4.316 - c2.379,0,4.316-1.935,4.316-4.316c0-0.811-0.23-1.57-0.62-2.217L93.583,8.49c0.346,0.091,0.703,0.143,1.076,0.143 - c2.38,0,4.316-1.935,4.316-4.316S97.042,0,94.66,0z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-instagram.svg b/_private/svg/llms-icon-instagram.svg deleted file mode 100644 index 2ded6d4aca..0000000000 --- a/_private/svg/llms-icon-instagram.svg +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.42px" height="96.418px" viewBox="0 0 96.42 96.418" enable-background="new 0 0 96.42 96.418" xml:space="preserve"> -<circle fill="#FFFFFF" cx="48.21" cy="48.209" r="48.21"/> -<path fill="#FFFFFF" d="M48.656,5.578c-23.662,0-42.85,19.188-42.85,42.854c0,23.672,19.188,42.855,42.85,42.855 - c23.672,0,42.854-19.184,42.854-42.855C91.51,24.767,72.328,5.578,48.656,5.578z"/> -<g> - <path fill="#41A7DE" d="M48.154,57.057c4.811,0,8.729-3.914,8.729-8.725c0-1.905-0.619-3.658-1.648-5.088 - c-1.584-2.205-4.166-3.639-7.076-3.639c-2.916,0-5.49,1.434-7.08,3.637c-1.033,1.43-1.646,3.185-1.648,5.09 - C39.422,53.143,43.342,57.057,48.154,57.057z"/> - <polygon fill="#41A7DE" points="67.213,37.655 67.213,30.334 67.213,29.246 66.119,29.247 58.807,29.271 58.834,37.682 "/> - <path fill="#41A7DE" d="M48.154,4.729c-24.039,0-43.602,19.56-43.602,43.603c0,24.037,19.562,43.598,43.602,43.598 - s43.6-19.561,43.6-43.598C91.754,24.289,72.199,4.729,48.154,4.729z M72.949,43.244v20.299c0,5.287-4.295,9.588-9.582,9.588H32.943 - c-5.287,0-9.586-4.301-9.586-9.588V43.244V33.117c0-5.287,4.299-9.584,9.586-9.584h30.424c5.287,0,9.582,4.297,9.582,9.584V43.244z - "/> - <path fill="#41A7DE" d="M61.715,48.332c0,7.473-6.082,13.558-13.561,13.558c-7.48,0-13.559-6.085-13.559-13.558 - c0-1.802,0.354-3.521,0.994-5.088h-7.402v20.299c0,2.627,2.133,4.752,4.754,4.752h30.426c2.615,0,4.752-2.125,4.752-4.752V43.244 - h-7.41C61.354,44.811,61.715,46.53,61.715,48.332z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-lightbulb.svg b/_private/svg/llms-icon-lightbulb.svg deleted file mode 100644 index ac4d827f84..0000000000 --- a/_private/svg/llms-icon-lightbulb.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.03px" height="132.97px" viewBox="0 0 98.03 132.97" enable-background="new 0 0 98.03 132.97" xml:space="preserve"> -<path d="M73.946,95.57H24.083c-2.298,0-4.155-1.857-4.155-4.155s1.857-4.155,4.155-4.155h49.862c2.298,0,4.154,1.857,4.154,4.155 - S76.244,95.57,73.946,95.57z M78.1,103.884c0-2.301-1.856-4.156-4.154-4.156H24.083c-2.298,0-4.155,1.855-4.155,4.156 - c0,2.298,1.857,4.155,4.155,4.155h49.862C76.244,108.039,78.1,106.182,78.1,103.884z M69.791,116.35 - c0-2.296-1.857-4.156-4.155-4.156H32.394c-2.298,0-4.155,1.86-4.155,4.156c0,2.298,1.857,4.154,4.155,4.154h33.242 - C67.933,120.504,69.791,118.647,69.791,116.35z M18.555,82.115c1.707-1.531,1.843-4.161,0.302-5.868 - C4.102,59.896,4.947,35.099,20.779,19.801c15.835-15.307,40.645-15.307,56.471,0c15.836,15.298,16.678,40.094,1.923,56.446 - c-1.544,1.707-1.405,4.337,0.299,5.868c1.708,1.535,4.337,1.396,5.871-0.3c17.771-19.695,16.757-49.562-2.312-67.987 - c-19.066-18.438-48.95-18.438-68.026,0C-4.07,32.253-5.085,62.12,12.689,81.815c0.82,0.911,1.945,1.371,3.084,1.371 - C16.763,83.187,17.763,82.83,18.555,82.115z M61.48,128.814c0-2.298-1.856-4.155-4.154-4.155H40.704 - c-2.298,0-4.155,1.857-4.155,4.155c0,2.296,1.857,4.155,4.155,4.155h16.622C59.624,132.97,61.48,131.11,61.48,128.814z"/> -</svg> diff --git a/_private/svg/llms-icon-linkedin.svg b/_private/svg/llms-icon-linkedin.svg deleted file mode 100644 index eb2b4d370a..0000000000 --- a/_private/svg/llms-icon-linkedin.svg +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.421px" height="96.418px" viewBox="0 0 96.421 96.418" enable-background="new 0 0 96.421 96.418" xml:space="preserve"> -<circle fill="#FFFFFF" cx="48.211" cy="48.209" r="48.211"/> -<path fill="#449ED8" d="M51.203,45.278v-0.087c-0.016,0.031-0.039,0.062-0.061,0.087H51.203z"/> -<path fill="#41A7DE" d="M48.634,5.578c-23.669,0-42.854,19.185-42.854,42.85c0,23.673,19.186,42.855,42.854,42.855 - c23.667,0,42.856-19.183,42.856-42.855C91.49,24.763,72.3,5.578,48.634,5.578z M36.935,68.908h-9.187V41.279h9.187V68.908z - M32.342,37.5h-0.06c-3.081,0-5.075-2.121-5.075-4.773c0-2.711,2.056-4.779,5.193-4.779c3.145,0,5.082,2.068,5.141,4.779 - C37.541,35.379,35.545,37.5,32.342,37.5z M70.058,68.908h-9.186V54.127c0-3.716-1.326-6.25-4.654-6.25 - c-2.533,0-4.049,1.715-4.711,3.363c-0.246,0.59-0.305,1.404-0.305,2.237v15.431H42.02c0,0,0.118-25.041,0-27.629h9.183v3.912 - c1.223-1.89,3.408-4.562,8.281-4.562c6.039,0,10.574,3.944,10.574,12.432V68.908z"/> -</svg> diff --git a/_private/svg/llms-icon-lock.svg b/_private/svg/llms-icon-lock.svg deleted file mode 100644 index 0e6e321254..0000000000 --- a/_private/svg/llms-icon-lock.svg +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="76.861px" height="100px" viewBox="0 0 76.861 100" enable-background="new 0 0 76.861 100" xml:space="preserve"> -<g> - <path d="M71.409,40.384h-1.949v-9.12c0-16.867-13.386-30.958-30.115-31.258c-0.457-0.008-1.37-0.008-1.828,0 - C20.786,0.307,7.4,14.397,7.4,31.264v9.12H5.452C2.45,40.384,0,43.483,0,47.31v45.739C0,96.871,2.45,100,5.452,100h65.957 - c3.004,0,5.452-3.129,5.452-6.951V47.31C76.861,43.483,74.413,40.384,71.409,40.384z M44.609,70.133v13.821 - c0,1.582-1.323,2.922-2.908,2.922h-6.54c-1.585,0-2.909-1.34-2.909-2.922V70.133c-1.537-1.512-2.431-3.603-2.431-5.916 - c0-4.382,3.389-8.149,7.695-8.324c0.458-0.018,1.371-0.018,1.828,0c4.309,0.175,7.695,3.942,7.695,8.324 - C47.04,66.53,46.146,68.621,44.609,70.133z M56.617,40.384H39.344h-1.828H20.244v-9.12c0-10.047,8.166-18.355,18.187-18.355 - s18.187,8.309,18.187,18.355V40.384L56.617,40.384z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-media.svg b/_private/svg/llms-icon-media.svg deleted file mode 100644 index 8642eaecbb..0000000000 --- a/_private/svg/llms-icon-media.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100.438px" height="76.853px" viewBox="0 0 100.438 76.853" enable-background="new 0 0 100.438 76.853" - xml:space="preserve"> -<g> - <g> - <path d="M89.695,0H10.746C4.819,0,0,4.828,0,10.75v55.359c0,5.923,4.819,10.743,10.746,10.743h78.949 - c5.919,0,10.742-4.82,10.742-10.743V10.75C100.438,4.828,95.614,0,89.695,0z M95.322,66.109c0,3.106-2.521,5.636-5.627,5.636 - H10.746c-3.101,0-5.63-2.529-5.63-5.636V10.75c0-3.105,2.529-5.635,5.63-5.635h78.949c3.098,0,5.627,2.529,5.627,5.635V66.109z"/> - <path d="M50.219,12.917c-14.091,0-25.509,11.426-25.509,25.513c0,14.087,11.418,25.505,25.509,25.505 - c14.087,0,25.513-11.41,25.513-25.505C75.731,24.343,64.306,12.917,50.219,12.917z M43.299,52.271V24.599L61.785,38.43 - L43.299,52.271z"/> - </g> -</g> -</svg> diff --git a/_private/svg/llms-icon-member.svg b/_private/svg/llms-icon-member.svg deleted file mode 100644 index 29296c5204..0000000000 --- a/_private/svg/llms-icon-member.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.062px" height="135.368px" viewBox="0 0 98.062 135.368" enable-background="new 0 0 98.062 135.368" - xml:space="preserve"> -<path d="M49.041,57.537c15.889,0,28.775-12.875,28.775-28.761C77.815,12.885,64.93,0,49.041,0C33.149,0,20.273,12.885,20.273,28.776 - C20.273,44.663,33.149,57.537,49.041,57.537z M61.222,59.5H36.829C16.522,59.5,0,76.019,0,96.338v29.834l0.08,0.47l2.055,0.655 - c19.389,6.039,36.221,8.071,50.08,8.071c27.07,0,42.757-7.726,43.729-8.213l1.929-0.973h0.188V96.338 - C98.081,76.019,81.543,59.5,61.222,59.5z"/> -</svg> diff --git a/_private/svg/llms-icon-new-lesson.svg b/_private/svg/llms-icon-new-lesson.svg deleted file mode 100644 index 5c77d519fd..0000000000 --- a/_private/svg/llms-icon-new-lesson.svg +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="50.926px" height="60.185px" viewBox="25.833 4.352 50.926 60.185" enable-background="new 25.833 4.352 50.926 60.185" - xml:space="preserve"> -<path d="M25.833,4.352h33.565L76.76,21.713v42.824H25.833V4.352z M30.463,8.981v50.926h41.666V27.5H53.611V8.981H30.463z - M58.24,10.139V22.87h12.732L58.24,10.139z"/> -</svg> diff --git a/_private/svg/llms-icon-paper.svg b/_private/svg/llms-icon-paper.svg deleted file mode 100644 index 33d0ee01d8..0000000000 --- a/_private/svg/llms-icon-paper.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100.008px" viewBox="0 0 100 100.008" enable-background="new 0 0 100 100.008" xml:space="preserve"> -<g> - <g> - <path d="M0,0v100.008h65.267L100,65.269V0H0L0,0z M4.65,95.35V4.647h90.701v58.692l-0.961,0.959H64.306v30.091l-0.969,0.963H4.65 - V95.35z"/> - <rect x="16.528" y="15.283" width="66.945" height="4.769"/> - <rect x="16.528" y="30.826" width="66.945" height="4.768"/> - <rect x="16.528" y="46.376" width="33.375" height="4.768"/> - <rect x="16.528" y="61.911" width="16.685" height="4.768"/> - </g> -</g> -</svg> diff --git a/_private/svg/llms-icon-papers.svg b/_private/svg/llms-icon-papers.svg deleted file mode 100644 index 971fbb8a3f..0000000000 --- a/_private/svg/llms-icon-papers.svg +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="86.048px" height="100.557px" viewBox="0 0 86.048 100.557" enable-background="new 0 0 86.048 100.557" - xml:space="preserve"> -<path d="M83.773,25.916h-2.274c-1.257,0-2.274,1.021-2.274,2.28c0,1.251,1.018,2.272,2.274,2.272v65.545H30.472 - c-0.002-1.26-1.02-2.281-2.276-2.281c-1.255,0-2.274,1.021-2.274,2.281v2.271c0,1.26,1.02,2.272,2.274,2.272h55.578 - c1.257,0,2.274-1.013,2.274-2.272V28.196C86.048,26.937,85.03,25.916,83.773,25.916z"/> -<path d="M77.409,89.647V19.549c0-1.251-1.02-2.272-2.276-2.272h-2.274c-1.255,0-2.274,1.021-2.274,2.272 - c0,1.259,1.018,2.28,2.274,2.28v65.537H21.832c0-1.251-1.02-2.272-2.276-2.272c-1.255,0-2.274,1.021-2.274,2.28v2.272 - c0,1.251,1.02,2.272,2.274,2.272h55.578C76.39,91.919,77.409,90.898,77.409,89.647z"/> -<path d="M68.768,81.008V10.911c0-1.259-1.02-2.272-2.274-2.272h-7.54c-1.257,0-2.276,1.013-2.276,2.272 - c0,1.259,1.02,2.28,2.276,2.28h5.263v65.537H13.191c-0.002-1.251-1.02-2.272-2.274-2.272c-1.259,0-2.276,1.021-2.276,2.272v2.28 - c0,1.251,1.017,2.272,2.276,2.272h55.576C67.748,83.281,68.768,82.259,68.768,81.008z"/> -<path d="M60.127,72.362V18.908c0-0.08-0.004-0.15-0.01-0.23c-0.006-0.055-0.017-0.111-0.026-0.158 - c-0.002-0.024-0.004-0.04-0.008-0.063c-0.012-0.063-0.031-0.126-0.05-0.19c-0.002-0.008-0.002-0.016-0.004-0.024 - c-0.02-0.063-0.043-0.126-0.069-0.19c-0.002,0-0.004-0.016-0.006-0.016c-0.025-0.063-0.053-0.119-0.081-0.174 - c-0.004-0.008-0.01-0.024-0.014-0.032c-0.027-0.047-0.058-0.095-0.087-0.143c-0.01-0.016-0.018-0.031-0.03-0.047 - c-0.027-0.04-0.059-0.079-0.091-0.126c-0.014-0.016-0.025-0.032-0.041-0.056c-0.036-0.04-0.071-0.079-0.109-0.119 - c-0.014-0.016-0.025-0.032-0.041-0.04L42.826,0.665c-0.014-0.016-0.03-0.023-0.043-0.04c-0.04-0.04-0.078-0.079-0.119-0.111 - c-0.018-0.016-0.04-0.032-0.057-0.04c-0.04-0.032-0.081-0.063-0.123-0.095c-0.016-0.008-0.031-0.016-0.045-0.023 - c-0.049-0.032-0.095-0.063-0.146-0.087c-0.01-0.008-0.021-0.016-0.032-0.016c-0.055-0.032-0.111-0.056-0.17-0.079 - c-0.006-0.008-0.014-0.008-0.02-0.008c-0.063-0.032-0.125-0.048-0.188-0.071c-0.008,0-0.016,0-0.024-0.008 - c-0.063-0.016-0.126-0.031-0.19-0.047c-0.02,0-0.04,0-0.059-0.008c-0.054-0.008-0.107-0.016-0.163-0.024C41.371,0,41.296,0,41.219,0 - H2.274C1.02,0,0,1.014,0,2.272v70.089c0,1.259,1.02,2.28,2.274,2.28h55.578C59.109,74.642,60.127,73.621,60.127,72.362z - M43.493,7.768l8.866,8.86h-8.866V7.768z M4.551,70.089V4.545h34.392v14.363c0,1.251,1.02,2.272,2.274,2.272h14.359v48.909H4.551z" - /> -</svg> diff --git a/_private/svg/llms-icon-play.svg b/_private/svg/llms-icon-play.svg deleted file mode 100644 index 84f24e06af..0000000000 --- a/_private/svg/llms-icon-play.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> -<g> - <g id="_x33_56._Play_1_"> - <g> - <path d="M92.297,24.63C78.692,1.075,48.583-6.992,25.037,6.607C1.482,20.201-6.587,50.316,7.008,73.871 - c13.604,23.548,43.714,31.617,67.264,18.02C97.818,78.291,105.898,48.178,92.297,24.63z M69.35,83.358 - C50.514,94.237,26.421,87.783,15.542,68.94C4.66,50.103,11.121,26.012,29.956,15.138C48.794,4.26,72.888,10.715,83.763,29.554 - C94.642,48.391,88.186,72.478,69.35,83.358z M67.382,47.242l-25.789-15.03c-2.345-1.373-4.241-0.276-4.229,2.442l0.134,29.848 - c0.013,2.718,1.932,3.821,4.289,2.461L67.375,52.19C69.721,50.831,69.728,48.622,67.382,47.242z"/> - </g> - </g> -</g> -</svg> diff --git a/_private/svg/llms-icon-plus.svg b/_private/svg/llms-icon-plus.svg deleted file mode 100644 index 68e72da1aa..0000000000 --- a/_private/svg/llms-icon-plus.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> -<g> - <path d="M90.897,40.873H59.097V9.104C59.097,4.078,55.042,0,50.011,0c-5.028,0-9.084,4.078-9.084,9.106v31.788H9.116 - C4.087,40.894-0.002,44.97,0,50c-0.002,2.514,1.013,4.817,2.659,6.461c1.647,1.651,3.921,2.694,6.432,2.694h31.836v31.742 - c0,2.514,1,4.793,2.647,6.437c1.647,1.646,3.915,2.665,6.429,2.665c5.029,0,9.094-4.078,9.094-9.102V59.152h31.801 - c5.027,0,9.107-4.112,9.102-9.141C99.999,44.985,95.921,40.873,90.897,40.873z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-question.svg b/_private/svg/llms-icon-question.svg deleted file mode 100644 index 96a968e830..0000000000 --- a/_private/svg/llms-icon-question.svg +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100px" height="100.001px" viewBox="0 0 100 100.001" enable-background="new 0 0 100 100.001" xml:space="preserve"> -<g> - <path d="M49.333,0.004C21.72,0.374-0.363,23.06,0.004,50.673c0.37,27.601,23.054,49.691,50.665,49.323 - c27.605-0.372,49.693-23.058,49.326-50.669C99.625,21.724,76.941-0.364,49.333,0.004z M49.184,80.435l-0.276-0.004 - c-4.252-0.126-7.25-3.259-7.129-7.447c0.119-4.118,3.189-7.107,7.301-7.107l0.247,0.005c4.37,0.129,7.335,3.231,7.212,7.54 - C56.417,77.551,53.394,80.435,49.184,80.435z M67.07,44.937c-1,1.42-3.199,3.185-5.969,5.343l-3.052,2.106 - c-1.675,1.302-2.686,2.527-3.065,3.734c-0.299,0.947-0.445,1.198-0.471,3.129l-0.005,0.49h-11.65l0.034-0.985 - c0.143-4.053,0.242-6.437,1.922-8.406c2.634-3.094,8.446-6.836,8.693-6.994c0.832-0.628,1.535-1.342,2.058-2.104 - c1.223-1.686,1.764-3.014,1.764-4.317c0-1.81-0.536-3.483-1.599-4.974c-1.021-1.438-2.96-2.166-5.764-2.166 - c-2.782,0-4.687,0.882-5.826,2.692c-1.171,1.862-1.764,3.818-1.764,5.815v0.497H30.365l0.021-0.519 - c0.311-7.357,2.936-12.655,7.802-15.747c3.058-1.969,6.863-2.966,11.302-2.966c5.811,0,10.717,1.412,14.578,4.196 - c3.913,2.821,5.898,7.048,5.898,12.562C69.966,39.407,68.992,42.304,67.07,44.937z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-rightarrow.svg b/_private/svg/llms-icon-rightarrow.svg deleted file mode 100644 index b41f94050f..0000000000 --- a/_private/svg/llms-icon-rightarrow.svg +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="58.862px" height="100.225px" viewBox="0 0 58.862 100.225" enable-background="new 0 0 58.862 100.225" - xml:space="preserve"> -<path d="M57.36,46.492L12.37,1.508c-2-2.011-5.25-2.011-7.247,0L1.5,5.124c-2,2.003-2,5.25,0,7.247l37.737,37.743L1.5,87.845 - c-2,2.01-2,5.251,0,7.261l3.624,3.616c1.996,2.004,5.247,2.004,7.247,0L57.36,53.738C59.363,51.734,59.363,48.487,57.36,46.492z"/> -</svg> diff --git a/_private/svg/llms-icon-search.svg b/_private/svg/llms-icon-search.svg deleted file mode 100644 index b6c659b26a..0000000000 --- a/_private/svg/llms-icon-search.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.727px" height="98.726px" viewBox="0 0 98.727 98.726" enable-background="new 0 0 98.727 98.726" xml:space="preserve"> -<g id="Search"> - <path d="M96.311,84.64l-21.45-21.445c-0.113-0.111-0.247-0.192-0.367-0.303c4.222-6.397,6.685-14.062,6.685-22.304 - C81.179,18.174,63.004,0,40.59,0C18.174,0,0,18.174,0,40.588s18.171,40.594,40.588,40.594c8.243,0,15.906-2.466,22.306-6.686 - c0.106,0.121,0.187,0.252,0.3,0.363l21.449,21.446c3.224,3.227,8.442,3.227,11.668,0C99.533,93.089,99.533,87.866,96.311,84.64z - M40.59,67.105c-14.647,0-26.52-11.872-26.52-26.518S25.943,14.07,40.59,14.07c14.643,0,26.515,11.872,26.515,26.518 - S55.233,67.105,40.59,67.105z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-target.svg b/_private/svg/llms-icon-target.svg deleted file mode 100644 index 0df9f3ea7c..0000000000 --- a/_private/svg/llms-icon-target.svg +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="101.105px" height="101.103px" viewBox="0 0 101.105 101.103" enable-background="new 0 0 101.105 101.103" - xml:space="preserve"> -<path d="M76.976,34.705l-4.366,4.371c2.049,3.771,3.218,8.063,3.218,12.643c0,14.576-11.869,26.444-26.444,26.444 - S22.941,66.295,22.941,51.719c0-14.575,11.867-26.441,26.442-26.441c4.58,0,8.873,1.167,12.646,3.216l4.369-4.369 - c-4.954-3.069-10.774-4.833-17.015-4.833c-17.884,0-32.428,14.544-32.428,32.427c0,17.882,14.544,32.428,32.428,32.428 - c17.881,0,32.427-14.544,32.427-32.428C81.811,45.48,80.045,39.658,76.976,34.705z M64.841,46.855l-5.25,5.237 - c-0.195,5.479-4.686,9.846-10.208,9.846c-5.643,0-10.22-4.577-10.22-10.22c0-5.521,4.369-10.009,9.848-10.203l5.252-5.24 - c-1.544-0.495-3.174-0.765-4.878-0.765c-8.937,0-16.208,7.276-16.208,16.208c0,8.936,7.271,16.209,16.208,16.209 - c8.932,0,16.206-7.273,16.206-16.209C65.589,50.03,65.335,48.385,64.841,46.855z M91.719,26.324l-4.372,4.371 - c3.46,6.225,5.433,13.394,5.433,21.024c0,23.929-19.467,43.398-43.396,43.398c-23.928,0-43.398-19.47-43.398-43.398 - c0-23.925,19.47-43.397,43.398-43.397c7.632,0,14.799,1.976,21.024,5.435l4.372-4.369c-7.425-4.476-16.118-7.051-25.396-7.051 - C22.148,2.336,0,24.482,0,51.719c0,27.235,22.148,49.384,49.383,49.384s49.383-22.148,49.383-49.384 - C98.767,42.439,96.191,33.748,91.719,26.324z M85.233,26.453l-5.716-0.636L51.371,53.963c-1.169,1.17-3.065,1.17-4.234,0 - c-1.167-1.166-1.167-3.06,0-4.229l28.146-28.148l-0.632-5.711L90.523,0l2.115,8.466l8.467,2.113L85.233,26.453z"/> -</svg> diff --git a/_private/svg/llms-icon-twitter.svg b/_private/svg/llms-icon-twitter.svg deleted file mode 100644 index c1d3cebb54..0000000000 --- a/_private/svg/llms-icon-twitter.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.42px" height="96.418px" viewBox="0 0 96.42 96.418" enable-background="new 0 0 96.42 96.418" xml:space="preserve"> -<circle fill="#FFFFFF" cx="48.21" cy="48.209" r="48.21"/> -<path fill="#41A7DE" d="M47.882,5.466C24.216,5.466,5.03,24.655,5.03,48.322c0,23.667,19.186,42.852,42.853,42.852 - c23.668,0,42.855-19.185,42.855-42.852C90.738,24.655,71.55,5.466,47.882,5.466z M64.925,39.584c0.018,0.375,0.024,0.75,0.024,1.133 - c0,11.618-8.843,25.019-25.016,25.019c-4.967,0-9.587-1.456-13.477-3.951c0.688,0.078,1.388,0.118,2.095,0.118 - c4.123,0,7.91-1.401,10.918-3.761c-3.845-0.072-7.09-2.613-8.209-6.104c0.536,0.098,1.087,0.156,1.653,0.156 - c0.804,0,1.578-0.109,2.315-0.307c-4.018-0.812-7.053-4.363-7.053-8.622c0-0.036,0-0.071,0-0.107 - c1.188,0.657,2.543,1.049,3.984,1.094c-2.359-1.574-3.91-4.268-3.91-7.317c0-1.611,0.434-3.122,1.19-4.423 - c4.335,5.322,10.813,8.824,18.12,9.187c-0.148-0.641-0.226-1.313-0.226-2.004c0-4.855,3.937-8.786,8.794-8.786 - c2.525,0,4.812,1.066,6.414,2.771c2.002-0.392,3.885-1.123,5.586-2.133c-0.66,2.053-2.053,3.776-3.866,4.864 - c1.776-0.206,3.472-0.686,5.048-1.381C68.132,36.793,66.642,38.345,64.925,39.584z"/> -</svg> diff --git a/_private/svg/llms-icon-users.svg b/_private/svg/llms-icon-users.svg deleted file mode 100644 index e7abbd1aa6..0000000000 --- a/_private/svg/llms-icon-users.svg +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="100.188px" height="84.312px" viewBox="0 0 100.188 84.312" enable-background="new 0 0 100.188 84.312" - xml:space="preserve"> -<path d="M60.464,14.467c4.635,2.909,7.881,7.823,8.467,13.527c1.894,0.887,3.991,1.393,6.209,1.393 - c8.12,0,14.698-6.582,14.698-14.698S83.26,0,75.14,0C67.104,0,60.584,6.458,60.464,14.467z M50.835,44.555 - c8.116,0,14.698-6.573,14.698-14.689s-6.582-14.698-14.698-14.698S36.142,21.75,36.142,29.866S42.719,44.555,50.835,44.555z - M57.062,45.557H44.6c-10.374,0-18.814,8.436-18.814,18.814v15.248l0.04,0.239l1.051,0.328c9.903,3.087,18.499,4.125,25.582,4.125 - c13.829,0,21.838-3.947,22.335-4.196l0.984-0.496h0.098V64.371C75.885,53.993,67.44,45.557,57.062,45.557z M81.375,30.389H69.01 - c-0.133,4.95-2.253,9.411-5.592,12.613c9.22,2.741,15.966,11.292,15.966,21.395v4.692c12.219-0.443,19.262-3.912,19.723-4.143 - l0.98-0.497h0.102V49.203C100.188,38.825,91.749,30.389,81.375,30.389z M25.049,29.387c2.878,0,5.553-0.834,7.819-2.271 - c0.723-4.702,3.242-8.799,6.834-11.594c0.013-0.275,0.044-0.55,0.044-0.825C39.748,6.582,33.166,0,25.049,0 - C16.938,0,10.36,6.582,10.36,14.698S16.938,29.387,25.049,29.387z M38.249,43.002c-3.327-3.193-5.429-7.628-5.584-12.551 - c-0.461-0.026-0.909-0.062-1.379-0.062H18.813C8.44,30.389,0,38.825,0,49.203V64.45l0.04,0.24l1.051,0.328 - c7.948,2.475,15.031,3.619,21.191,3.982v-4.604C22.282,54.294,29.023,45.744,38.249,43.002z"/> -</svg> diff --git a/_private/svg/llms-icon-view.svg b/_private/svg/llms-icon-view.svg deleted file mode 100644 index 730ef22729..0000000000 --- a/_private/svg/llms-icon-view.svg +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="98.5px" height="98.5px" viewBox="0 0 98.5 98.5" enable-background="new 0 0 98.5 98.5" xml:space="preserve"> -<g> - <g id="curved_x5F_arrow"> - <path d="M54.559,24.021c-15.641-0.298-28.567,12.146-28.863,27.787c-0.297,15.639,12.146,28.564,27.786,28.863 - c-11.175-0.213-20.061-9.443-19.847-20.617c0.21-11.176,9.441-20.061,20.616-19.847l4.047,0.076l-0.155,8.094l16.493-15.879 - L58.757,16.005l-0.152,8.093L54.559,24.021z"/> - </g> -</g> -<g> - <path d="M49.25,0C22.049,0,0,22.049,0,49.25S22.049,98.5,49.25,98.5S98.5,76.451,98.5,49.25S76.451,0,49.25,0z M49.25,97.359 - c-26.571,0-48.11-21.539-48.11-48.109c0-26.571,21.54-48.11,48.11-48.11c26.57,0,48.109,21.54,48.109,48.11 - C97.359,75.82,75.82,97.359,49.25,97.359z"/> -</g> -</svg> diff --git a/_private/svg/llms-icon-youtube.svg b/_private/svg/llms-icon-youtube.svg deleted file mode 100644 index 543bc31e90..0000000000 --- a/_private/svg/llms-icon-youtube.svg +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="96.42px" height="96.418px" viewBox="0 0 96.42 96.418" enable-background="new 0 0 96.42 96.418" xml:space="preserve"> -<circle fill="#FFFFFF" cx="48.21" cy="48.209" r="48.21"/> -<path fill="#449ED8" d="M48.279,41.904c0.332,0,0.594-0.09,0.789-0.271c0.195-0.184,0.295-0.439,0.295-0.757v-6.509 - c0-0.265-0.1-0.473-0.301-0.634c-0.197-0.163-0.457-0.245-0.783-0.245c-0.295,0-0.539,0.082-0.725,0.245 - c-0.186,0.161-0.279,0.369-0.279,0.634v6.509c0,0.327,0.09,0.577,0.262,0.757C47.711,41.818,47.957,41.904,48.279,41.904z"/> -<path fill="#449ED8" d="M53.666,53.926c-0.334,0-0.668,0.08-1.002,0.255c-0.322,0.173-0.635,0.43-0.926,0.754v-4.733h-2.193v14.705 - h2.193v-0.832c0.281,0.331,0.594,0.574,0.926,0.731c0.332,0.164,0.707,0.235,1.133,0.235c0.643,0,1.135-0.2,1.475-0.613 - c0.344-0.412,0.512-0.998,0.512-1.76v-6.023c0-0.889-0.182-1.564-0.541-2.029C54.877,54.153,54.355,53.926,53.666,53.926z - M53.547,62.361c0,0.352-0.061,0.598-0.184,0.75c-0.129,0.157-0.32,0.232-0.586,0.232c-0.186,0-0.357-0.039-0.525-0.121 - c-0.166-0.07-0.34-0.205-0.514-0.38v-6.757c0.146-0.15,0.295-0.262,0.447-0.334c0.152-0.07,0.305-0.105,0.457-0.105 - c0.293,0,0.518,0.096,0.678,0.279c0.154,0.191,0.227,0.467,0.227,0.84V62.361z"/> -<polygon fill="#449ED8" points="33.275,52.341 35.803,52.341 35.803,64.906 38.242,64.906 38.242,52.341 40.775,52.341 - 40.775,50.201 33.275,50.201 "/> -<path fill="#449ED8" d="M45.748,62.292c-0.205,0.237-0.426,0.43-0.674,0.587c-0.242,0.151-0.449,0.226-0.602,0.226 - c-0.203,0-0.348-0.052-0.445-0.172c-0.088-0.116-0.137-0.296-0.137-0.553v-8.324h-2.166v9.069c0,0.649,0.129,1.125,0.381,1.451 - c0.252,0.33,0.631,0.487,1.133,0.487c0.408,0,0.826-0.112,1.258-0.347c0.434-0.231,0.854-0.566,1.252-1.005v1.194h2.166V54.056 - h-2.166V62.292z"/> -<path fill="#41A7DE" d="M48.658,5.578c-23.664,0-42.852,19.188-42.852,42.854c0,23.672,19.188,42.855,42.852,42.855 - c23.67,0,42.854-19.184,42.854-42.855C91.512,24.767,72.328,5.578,48.658,5.578z M53.826,31.726h2.439v9.159 - c0,0.282,0.057,0.483,0.156,0.609c0.098,0.129,0.27,0.192,0.496,0.192c0.178,0,0.408-0.081,0.684-0.251 - c0.275-0.171,0.521-0.381,0.75-0.643v-9.067h2.445v11.946h-2.445V42.35c-0.445,0.482-0.914,0.863-1.402,1.113 - c-0.488,0.252-0.959,0.385-1.418,0.385c-0.568,0-0.986-0.183-1.275-0.541c-0.283-0.351-0.43-0.887-0.43-1.599V31.726z - M44.766,34.453c0-0.923,0.328-1.664,0.982-2.207c0.658-0.55,1.543-0.82,2.652-0.82c1.01,0,1.836,0.287,2.484,0.861 - c0.641,0.58,0.963,1.328,0.963,2.234v6.173c0,1.022-0.314,1.819-0.945,2.407c-0.635,0.582-1.506,0.872-2.615,0.872 - c-1.066,0-1.922-0.302-2.564-0.903c-0.637-0.601-0.957-1.406-0.957-2.422V34.453z M38.115,27.477l1.785,6.477h0.172l1.701-6.477 - h2.791l-3.199,9.479v6.717h-2.746v-6.42l-3.27-9.775H38.115z M70.084,61.562c0,4.32-3.506,7.83-7.828,7.83H35.057 - c-4.324,0-7.824-3.51-7.824-7.83v-6.294c0-4.319,3.5-7.829,7.824-7.829h27.199c4.322,0,7.828,3.51,7.828,7.829V61.562z"/> -<path fill="#449ED8" d="M60.213,53.789c-0.973,0-1.758,0.293-2.371,0.884c-0.619,0.591-0.924,1.366-0.924,2.301v4.87 - c0,1.049,0.283,1.868,0.838,2.457c0.555,0.605,1.324,0.896,2.293,0.896c1.082,0,1.896-0.277,2.436-0.837 - c0.547-0.563,0.816-1.401,0.816-2.517v-0.555h-2.232v0.492c0,0.639-0.076,1.053-0.213,1.241c-0.143,0.188-0.395,0.282-0.758,0.282 - c-0.342,0-0.59-0.113-0.734-0.33c-0.141-0.227-0.209-0.623-0.209-1.193V59.74h4.146v-2.767c0-1.029-0.266-1.814-0.799-2.363 - C61.969,54.06,61.205,53.789,60.213,53.789z M61.068,58.053h-1.914v-1.092c0-0.457,0.07-0.788,0.223-0.977 - c0.148-0.201,0.398-0.303,0.742-0.303c0.33,0,0.578,0.102,0.725,0.303c0.146,0.188,0.225,0.52,0.225,0.977V58.053z"/> -</svg> diff --git a/assets/js/llms-metabox-voucher.js b/assets/js/llms-metabox-voucher.js index 17787ab097..afd5a4e131 100644 --- a/assets/js/llms-metabox-voucher.js +++ b/assets/js/llms-metabox-voucher.js @@ -71,7 +71,7 @@ return changeNotSaved ? "If you leave this page you will lose your unsaved changes." : null; }; - $( 'input[type=submit]' ).click(function (e) { + $( 'input[type=submit][name=save]' ).click(function (e) { var unique_values = {}; var duplicate = false; $( 'input[name="llms_voucher_code[]"]' ).each(function() { @@ -88,8 +88,8 @@ return false; } - // if course or membership is not selected, don't allow user to save - if ( ! ($( '#_llms_voucher_courses' ).val() || $( '#_llms_voucher_membership' ).val())) { + // If course or membership is not selected, don't allow user to save. + if ( ! $( '#_llms_voucher_courses' ).val().length && ! $( '#_llms_voucher_membership' ).val().length ) { alert( 'Please select course or membership before saving.' ); return false; } diff --git a/assets/js/llms-metabox-voucher.min.js b/assets/js/llms-metabox-voucher.min.js index 40a9769431..2b97fe5f6c 100644 --- a/assets/js/llms-metabox-voucher.min.js +++ b/assets/js/llms-metabox-voucher.min.js @@ -1,2 +1,2 @@ -function llms_on_voucher_duplicate(e){if(e.length){for(var t=0;t<e.length;t++)jQuery('input[value="'+e[t].code+'"]').css("background-color","rgba(226, 96, 73, 0.6)");alert("Please make sure that there are no duplicate voucher codes.")}else jQuery("#post").submit()}!function(s){var r=[];function c(){for(var e="",t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",a=0;a<12;a++)e+=t.charAt(Math.floor(Math.random()*t.length));return e}s(document).ready(function(){var n=!1,o=0;function u(){s(".llms-voucher-delete").unbind("click"),s(".llms-voucher-delete").click(function(e){e.preventDefault();var t=s(this),a=t.data("id");n=!0,a?(r.push(a),s("#delete_ids").val(r.join(","))):o--,t.closest("tr").remove()})}s("#llms_voucher_add_codes").click(function(e){e.preventDefault();var t=s("#llms_voucher_add_quantity").val(),a=s("#llms_voucher_add_uses").val(),r="";if(n=!0,s.isNumeric(t)&&s.isNumeric(a)&&0<parseInt(t)&&0<parseInt(a)){if(50<t)return void alert("You can only generate 50 rows at a time");if(50<(o+=parseInt(t)))return alert("Please save before adding any more codes, limit is 50 at a time"),void(o-=parseInt(t));for(var l=1;l<=parseInt(t);l++)r+='<tr><td></td><td><input type="text" maxlength="20" placeholder="Code" value="'+c()+'" name="llms_voucher_code[]"><input type="hidden" name="llms_voucher_code_id[]" value="0"></td><td><span>0 / </span><input type="text" placeholder="Uses" value="'+a+'" class="llms-voucher-uses" name="llms_voucher_uses[]"></td><td><a href="#" class="llms-voucher-delete">'+delete_icon+"</a></td></tr>"}s("#llms_voucher_tbody").append(r),u()}),u(),s("#llms_voucher_tbody input").change(function(){n=!0}),s("#post").on("submit",function(){return"publish"===s("#publish").attr("name")&&s("<input />").attr("type","hidden").attr("name","publish").attr("value","true").appendTo("#post"),!0}),window.onbeforeunload=function(){return n?"If you leave this page you will lose your unsaved changes.":null},s("input[type=submit]").click(function(e){var t,a={},r=!1;return s('input[name="llms_voucher_code[]"]').each(function(){var e=s(this).val();a[e]?(s(this).css("background-color","rgba(226, 96, 73, 0.6)"),r=!0):a[e]=!0}),r?alert("Please make sure that there are no duplicate voucher codes."):s("#_llms_voucher_courses").val()||s("#_llms_voucher_membership").val()?(n=!1,t={action:"check_voucher_duplicate",postId:jQuery("#post_ID").val(),codes:function(){var e=[];return s('input[name="llms_voucher_code[]"]').each(function(){e.push(s(this).val())}),e}(),_ajax_nonce:window.llms.ajax_nonce},new Ajax("post",t,!1).check_voucher_duplicate()):alert("Please select course or membership before saving."),!1})})}(jQuery); +function llms_on_voucher_duplicate(e){if(e.length){for(var t=0;t<e.length;t++)jQuery('input[value="'+e[t].code+'"]').css("background-color","rgba(226, 96, 73, 0.6)");alert("Please make sure that there are no duplicate voucher codes.")}else jQuery("#post").submit()}!function(s){var r=[];function c(){for(var e="",t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",a=0;a<12;a++)e+=t.charAt(Math.floor(Math.random()*t.length));return e}s(document).ready(function(){var n=!1,o=0;function u(){s(".llms-voucher-delete").unbind("click"),s(".llms-voucher-delete").click(function(e){e.preventDefault();var t=s(this),a=t.data("id");n=!0,a?(r.push(a),s("#delete_ids").val(r.join(","))):o--,t.closest("tr").remove()})}s("#llms_voucher_add_codes").click(function(e){e.preventDefault();var t=s("#llms_voucher_add_quantity").val(),a=s("#llms_voucher_add_uses").val(),r="";if(n=!0,s.isNumeric(t)&&s.isNumeric(a)&&0<parseInt(t)&&0<parseInt(a)){if(50<t)return void alert("You can only generate 50 rows at a time");if(50<(o+=parseInt(t)))return alert("Please save before adding any more codes, limit is 50 at a time"),void(o-=parseInt(t));for(var l=1;l<=parseInt(t);l++)r+='<tr><td></td><td><input type="text" maxlength="20" placeholder="Code" value="'+c()+'" name="llms_voucher_code[]"><input type="hidden" name="llms_voucher_code_id[]" value="0"></td><td><span>0 / </span><input type="text" placeholder="Uses" value="'+a+'" class="llms-voucher-uses" name="llms_voucher_uses[]"></td><td><a href="#" class="llms-voucher-delete">'+delete_icon+"</a></td></tr>"}s("#llms_voucher_tbody").append(r),u()}),u(),s("#llms_voucher_tbody input").change(function(){n=!0}),s("#post").on("submit",function(){return"publish"===s("#publish").attr("name")&&s("<input />").attr("type","hidden").attr("name","publish").attr("value","true").appendTo("#post"),!0}),window.onbeforeunload=function(){return n?"If you leave this page you will lose your unsaved changes.":null},s("input[type=submit][name=save]").click(function(e){var t,a={},r=!1;return s('input[name="llms_voucher_code[]"]').each(function(){var e=s(this).val();a[e]?(s(this).css("background-color","rgba(226, 96, 73, 0.6)"),r=!0):a[e]=!0}),r?alert("Please make sure that there are no duplicate voucher codes."):s("#_llms_voucher_courses").val().length||s("#_llms_voucher_membership").val().length?(n=!1,t={action:"check_voucher_duplicate",postId:jQuery("#post_ID").val(),codes:function(){var e=[];return s('input[name="llms_voucher_code[]"]').each(function(){e.push(s(this).val())}),e}(),_ajax_nonce:window.llms.ajax_nonce},new Ajax("post",t,!1).check_voucher_duplicate()):alert("Please select course or membership before saving."),!1})})}(jQuery); //# sourceMappingURL=../maps/js/llms-metabox-voucher.min.js.map diff --git a/assets/maps/js/llms-metabox-voucher.min.js.map b/assets/maps/js/llms-metabox-voucher.min.js.map index ae801d0d0d..6569c5bf04 100644 --- a/assets/maps/js/llms-metabox-voucher.min.js.map +++ b/assets/maps/js/llms-metabox-voucher.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["llms-metabox-voucher.js"],"names":["llms_on_voucher_duplicate","results","length","i","jQuery","code","css","alert","submit","$","deleteIds","randomizeCode","text","possible","charAt","Math","floor","random","document","ready","changeNotSaved","codesAddedSinceLastSave","bindDeleteVoucherCode","unbind","click","e","preventDefault","t","this","old","data","push","val","join","closest","remove","qty","uses","html","isNumeric","parseInt","delete_icon","append","change","on","attr","appendTo","window","onbeforeunload","unique_values","duplicate","each","action","postId","codes","get_codes_from_inputs","_ajax_nonce","llms","ajax_nonce","Ajax","check_voucher_duplicate"],"mappings":"AAuKA,SAASA,0BAA2BC,GACnC,GAAIA,EAAQC,OAAQ,CACnB,IAAK,IAAIC,EAAI,EAAGA,EAAIF,EAAQC,OAAQC,IACnCC,OAAQ,gBAAkBH,EAAQE,GAAGE,KAAO,MAAOC,IAAK,mBAAoB,0BAE7EC,MAAO,oEAEPH,OAAQ,SAAUI,UA9KpB,SAAWC,GAEV,IAAIC,EAAY,GA0HhB,SAASC,IAIR,IAHA,IAAIC,EAAW,GACXC,EAAW,iEAENV,EAAI,EAAGA,EAAI,GAAIA,IACvBS,GAAQC,EAASC,OAAQC,KAAKC,MAAOD,KAAKE,SAAWJ,EAASX,SAG/D,OAAOU,EAhIRH,EAAGS,UAAWC,MAAM,WAEnB,IAAIC,GAA0B,EAC1BC,EAA0B,EA8F9B,SAASC,IACRb,EAAG,wBAAyBc,OAAQ,SACpCd,EAAG,wBAAyBe,MAAM,SAAUC,GAC3CA,EAAEC,iBAEF,IAAIC,EAAMlB,EAAGmB,MACTC,EAAMF,EAAEG,KAAM,MAElBV,GAAiB,EAEbS,GACHnB,EAAUqB,KAAMF,GAEhBpB,EAAG,eAAgBuB,IAAKtB,EAAUuB,KAAM,OAExCZ,IAIDM,EAAEO,QAAS,MAAOC,WA/GpB1B,EAAG,2BAA4Be,MAAM,SAAUC,GAC9CA,EAAEC,iBAEF,IAAIU,EAAO3B,EAAG,8BAA+BuB,MACzCK,EAAO5B,EAAG,0BAA2BuB,MACrCM,EAAO,GAIX,GAFAlB,GAAiB,EAEbX,EAAE8B,UAAWH,IAAS3B,EAAE8B,UAAWF,IAChB,EAAlBG,SAAUJ,IAAgC,EAAnBI,SAAUH,GAAY,CAEhD,GAAU,GAAND,EAEH,YADA7B,MAAO,2CAMR,GAA8B,IAF9Bc,GAA2BmB,SAAUJ,IAKpC,OAFA7B,MAAO,wEACPc,GAA2BmB,SAAUJ,IAItC,IAAK,IAAIjC,EAAI,EAAGA,GAAKqC,SAAUJ,GAAOjC,IACrCmC,GAAQ,gFAG0D3B,IAAkB,oKAGZ0B,EAAO,2GAC7BI,YAAc,iBAMnEhC,EAAG,uBAAwBiC,OAAQJ,GAEnChB,MAGDA,IAEAb,EAAG,6BAA8BkC,OAAO,WACvCvB,GAAiB,IAGlBX,EAAG,SAAUmC,GAAI,SAAU,WAO1B,MANuC,YAAnCnC,EAAG,YAAaoC,KAAM,SACzBpC,EAAG,aAAcoC,KAAM,OAAQ,UAC7BA,KAAM,OAAQ,WACdA,KAAM,QAAS,QACfC,SAAU,UAEN,IAGRC,OAAOC,eAAiB,WACvB,OAAO5B,EAAiB,6DAA+D,MAGxFX,EAAG,sBAAuBe,MAAM,SAAUC,GACzC,IAuEGK,EAvECmB,EAAgB,GAChBC,GAAgB,EAUpB,OATAzC,EAAG,qCAAsC0C,KAAK,WAC7C,IAAInB,EAAMvB,EAAGmB,MAAOI,MACbiB,EAAcjB,IAGpBvB,EAAGmB,MAAOtB,IAAK,mBAAoB,0BACnC4C,GAAY,GAHZD,EAAcjB,IAAO,IAMnBkB,EACH3C,MAAO,+DAKAE,EAAG,0BAA2BuB,OAASvB,EAAG,6BAA8BuB,OAKhFZ,GAAiB,EAiDdU,EAAO,CACVsB,OAAQ,0BAA2BC,OACnCjD,OAAQ,YAAa4B,MACrBsB,MAQF,WACC,IAAIA,EAAQ,GAKZ,OAJA7C,EAAG,qCAAsC0C,KAAK,WAC7CG,EAAMvB,KAAMtB,EAAGmB,MAAOI,SAGhBsB,EAdKC,GACXC,YAAaT,OAAOU,KAAKC,YAGf,IAAIC,KAAM,OAAQ7B,GAAM,GAC9B8B,2BA7DHrD,MAAO,sDALA,MAvFX,CAqKIH","file":"../../js/llms-metabox-voucher.min.js","sourcesContent":["(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]' ).click(function (e) {\n\t\t\tvar unique_values = {};\n\t\t\tvar duplicate = false;\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\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() || $( '#_llms_voucher_membership' ).val())) {\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\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 *\n\t * @return {void}\n\t */\n\tfunction check_voucher_duplicate() {\n\n\t\tvar data = {\n\t\t\taction: 'check_voucher_duplicate', 'postId' :\n\t\t\tjQuery( '#post_ID' ).val(),\n\t\t\t'codes' : get_codes_from_inputs(),\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"],"sourceRoot":"../../js"} \ No newline at end of file +{"version":3,"sources":["llms-metabox-voucher.js"],"names":["llms_on_voucher_duplicate","results","length","i","jQuery","code","css","alert","submit","$","deleteIds","randomizeCode","text","possible","charAt","Math","floor","random","document","ready","changeNotSaved","codesAddedSinceLastSave","bindDeleteVoucherCode","unbind","click","e","preventDefault","t","this","old","data","push","val","join","closest","remove","qty","uses","html","isNumeric","parseInt","delete_icon","append","change","on","attr","appendTo","window","onbeforeunload","unique_values","duplicate","each","action","postId","codes","get_codes_from_inputs","_ajax_nonce","llms","ajax_nonce","Ajax","check_voucher_duplicate"],"mappings":"AAuKA,SAASA,0BAA2BC,GACnC,GAAIA,EAAQC,OAAQ,CACnB,IAAK,IAAIC,EAAI,EAAGA,EAAIF,EAAQC,OAAQC,IACnCC,OAAQ,gBAAkBH,EAAQE,GAAGE,KAAO,MAAOC,IAAK,mBAAoB,0BAE7EC,MAAO,oEAEPH,OAAQ,SAAUI,UA9KpB,SAAWC,GAEV,IAAIC,EAAY,GA0HhB,SAASC,IAIR,IAHA,IAAIC,EAAW,GACXC,EAAW,iEAENV,EAAI,EAAGA,EAAI,GAAIA,IACvBS,GAAQC,EAASC,OAAQC,KAAKC,MAAOD,KAAKE,SAAWJ,EAASX,SAG/D,OAAOU,EAhIRH,EAAGS,UAAWC,MAAM,WAEnB,IAAIC,GAA0B,EAC1BC,EAA0B,EA8F9B,SAASC,IACRb,EAAG,wBAAyBc,OAAQ,SACpCd,EAAG,wBAAyBe,MAAM,SAAUC,GAC3CA,EAAEC,iBAEF,IAAIC,EAAMlB,EAAGmB,MACTC,EAAMF,EAAEG,KAAM,MAElBV,GAAiB,EAEbS,GACHnB,EAAUqB,KAAMF,GAEhBpB,EAAG,eAAgBuB,IAAKtB,EAAUuB,KAAM,OAExCZ,IAIDM,EAAEO,QAAS,MAAOC,WA/GpB1B,EAAG,2BAA4Be,MAAM,SAAUC,GAC9CA,EAAEC,iBAEF,IAAIU,EAAO3B,EAAG,8BAA+BuB,MACzCK,EAAO5B,EAAG,0BAA2BuB,MACrCM,EAAO,GAIX,GAFAlB,GAAiB,EAEbX,EAAE8B,UAAWH,IAAS3B,EAAE8B,UAAWF,IAChB,EAAlBG,SAAUJ,IAAgC,EAAnBI,SAAUH,GAAY,CAEhD,GAAU,GAAND,EAEH,YADA7B,MAAO,2CAMR,GAA8B,IAF9Bc,GAA2BmB,SAAUJ,IAKpC,OAFA7B,MAAO,wEACPc,GAA2BmB,SAAUJ,IAItC,IAAK,IAAIjC,EAAI,EAAGA,GAAKqC,SAAUJ,GAAOjC,IACrCmC,GAAQ,gFAG0D3B,IAAkB,oKAGZ0B,EAAO,2GAC7BI,YAAc,iBAMnEhC,EAAG,uBAAwBiC,OAAQJ,GAEnChB,MAGDA,IAEAb,EAAG,6BAA8BkC,OAAO,WACvCvB,GAAiB,IAGlBX,EAAG,SAAUmC,GAAI,SAAU,WAO1B,MANuC,YAAnCnC,EAAG,YAAaoC,KAAM,SACzBpC,EAAG,aAAcoC,KAAM,OAAQ,UAC7BA,KAAM,OAAQ,WACdA,KAAM,QAAS,QACfC,SAAU,UAEN,IAGRC,OAAOC,eAAiB,WACvB,OAAO5B,EAAiB,6DAA+D,MAGxFX,EAAG,iCAAkCe,MAAM,SAAUC,GACpD,IAuEGK,EAvECmB,EAAgB,GAChBC,GAAgB,EAUpB,OATAzC,EAAG,qCAAsC0C,KAAK,WAC7C,IAAInB,EAAMvB,EAAGmB,MAAOI,MACbiB,EAAcjB,IAGpBvB,EAAGmB,MAAOtB,IAAK,mBAAoB,0BACnC4C,GAAY,GAHZD,EAAcjB,IAAO,IAMnBkB,EACH3C,MAAO,+DAKDE,EAAG,0BAA2BuB,MAAM9B,QAAYO,EAAG,6BAA8BuB,MAAM9B,QAK9FkB,GAAiB,EAiDdU,EAAO,CACVsB,OAAQ,0BAA2BC,OACnCjD,OAAQ,YAAa4B,MACrBsB,MAQF,WACC,IAAIA,EAAQ,GAKZ,OAJA7C,EAAG,qCAAsC0C,KAAK,WAC7CG,EAAMvB,KAAMtB,EAAGmB,MAAOI,SAGhBsB,EAdKC,GACXC,YAAaT,OAAOU,KAAKC,YAGf,IAAIC,KAAM,OAAQ7B,GAAM,GAC9B8B,2BA7DHrD,MAAO,sDALA,MAvFX,CAqKIH","file":"../../js/llms-metabox-voucher.min.js","sourcesContent":["(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=save]' ).click(function (e) {\n\t\t\tvar unique_values = {};\n\t\t\tvar duplicate = false;\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\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\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 *\n\t * @return {void}\n\t */\n\tfunction check_voucher_duplicate() {\n\n\t\tvar data = {\n\t\t\taction: 'check_voucher_duplicate', 'postId' :\n\t\t\tjQuery( '#post_ID' ).val(),\n\t\t\t'codes' : get_codes_from_inputs(),\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"],"sourceRoot":"../../js"} \ No newline at end of file diff --git a/assets/scss/_includes/_buttons.scss b/assets/scss/_includes/_buttons.scss deleted file mode 100644 index a0fbe6d7b2..0000000000 --- a/assets/scss/_includes/_buttons.scss +++ /dev/null @@ -1,111 +0,0 @@ -.llms-button-action, -.llms-button-danger, -.llms-button-primary, -.llms-button-secondary { - border:none; - border-radius: 0; - color: $color-white; - cursor: pointer; - font-size: 16px; - font-weight: 300; - text-decoration: none; - text-shadow: none; - line-height: 1; - margin: 0; - max-width: 100%; - padding: 12px 24px; - position: relative; - transition: all .5s ease; - - &:disabled { - opacity: 0.5; - } - &:hover, &:active { - color: $color-white; - } - &:focus { - color: $color-white; - } - - &.auto { - width: auto; - } - - &.full { - display: block; - text-align: center; - width: 100%; - } - - &.square { - padding: 12px; - } - - &.small { - font-size: 13px; - padding: 8px 14px; - &.square { padding: 8px; } - } - - &.large { - font-size: 18px; - line-height: 1.2; - padding: 16px 32px; - &.square { padding: 16px; } - .fa { - left: -7px; - position: relative; - } - } - - -} - -.llms-button-primary { - background: $color-brand-blue; - &:hover, - &.clicked { - background: $color-brand-blue-dark; - } - &:focus, - &:active { - background: $color-brand-blue-light; - } -} - -.llms-button-secondary { - background: #e1e1e1; - color: #414141; - &:hover { - color: #414141; - background: darken( #e1e1e1, 8 ); - } - &:focus, - &:active { - color: #414141; - background: lighten( #e1e1e1, 4 ); - } -} - -.llms-button-action { - background: $color-brand-orange; - &:hover, - &.clicked { - background: $color-brand-orange-dark; - } - &:focus, - &:active { - background: $color-brand-orange-light; - } -} - -.llms-button-danger { - background: $color-danger; - &:hover { - background: darken( $color-danger, 8 ); - } - &:focus, - &:active { - background: lighten( $color-danger, 4 ); - } -} diff --git a/assets/scss/_includes/_extends.scss b/assets/scss/_includes/_extends.scss deleted file mode 100644 index 3ff4e48156..0000000000 --- a/assets/scss/_includes/_extends.scss +++ /dev/null @@ -1,30 +0,0 @@ -%cf, -%clearfix { - &:before, - &:after { - content: " "; - display: table; - } - - &:after { - clear: both; - } -} - - - -%llms-element { - background: $el-background; - box-shadow: $el-box-shadow; - display: block; - color: #212121; - min-height: 85px; - padding: 15px; - text-decoration: none; - position: relative; - transition: background .4s ease; - - &:hover { - background: $el-background-hover; - } -} diff --git a/assets/scss/_includes/_grid.scss b/assets/scss/_includes/_grid.scss deleted file mode 100644 index 611b6fdd41..0000000000 --- a/assets/scss/_includes/_grid.scss +++ /dev/null @@ -1,43 +0,0 @@ -.llms-cols { - @extend %clearfix; - - .llms-col { - width: 100%; - } - - @media all and (min-width: 600px) { - [class*="llms-col-"] { - float: left; - } - - $cols: 1; - @while $cols <= 12 { - .llms-col-#{$cols} { - width: 100% / $cols; - } - $cols: $cols + 1; - } - - } - -} - -.llms-flex-cols { - display: flex; - flex-flow: row wrap; - - [class*="llms-col"] { - flex: 0 1 auto; - width: 100%; - } - - @media all and (min-width: 600px) { - $cols: 1; - @while $cols <= 12 { - .llms-col-#{$cols} { - width: 100% / $cols; - } - $cols: $cols + 1; - } - } -} diff --git a/assets/scss/_includes/_llms-donut.scss b/assets/scss/_includes/_llms-donut.scss deleted file mode 100644 index f01a645b49..0000000000 --- a/assets/scss/_includes/_llms-donut.scss +++ /dev/null @@ -1,82 +0,0 @@ -.llms-donut { - - @include clearfix; - - background-color: #f1f1f1; - background-image: none; - border-radius: 50%; - color: $color-brand-pink; - height: 200px; - overflow: hidden; - position: relative; - width: 200px; - - svg { - overflow: visible !important; - pointer-events: none; - width: 100%; - } - - svg path { - fill: none; - stroke-width: 35px; - stroke: $color-brand-pink; - } - - &.mini { - height: 36px; - width: 36px; - .percentage { - font-size: 10px; - } - } - &.small { - height: 100px; - width: 100px; - .percentage { - font-size: 18px; - } - } - &.medium { - height: 130px; - width: 130px; - .percentage { - font-size: 26px; - } - } - &.large { - height: 260px; - width: 260px; - .percentage { - font-size: 48px; - } - } - - .inside { - align-items: center; - background: #fff; - border-radius: 50%; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - height: 80%; - justify-content: center; - left: 50%; - position: absolute; - text-align: center; - transform: translate(-50%, -50%); - width: 80%; - top: 50%; - z-index: 3; - } - - .percentage { - line-height: 1.2; - font-size: 34px; - } - - .caption { - font-size: 50%; - } - -} diff --git a/assets/scss/_includes/_llms-form-field.scss b/assets/scss/_includes/_llms-form-field.scss deleted file mode 100644 index 99c9757049..0000000000 --- a/assets/scss/_includes/_llms-form-field.scss +++ /dev/null @@ -1,213 +0,0 @@ -.llms-form-fields { - @extend %clearfix; - box-sizing: border-box; - & * { - box-sizing: border-box; - } - &.flush { - .llms-form-field { - padding: 0 0 10px; - } - } - - .wp-block-columns, .wp-block-column { - margin-bottom: 0; - } -} - - .llms-form-heading { - padding: 0 10px 10px; - } - - .llms-form-field { - float: left; - padding: 0 10px 10px; - position: relative; - width: 100%; - - // Ensure "empty" labels don't break the layout. - // See the billing_address_2 field which has no label. - label:empty:after { - content: '\00a0'; - } - - &.valid { - input[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 { - background: rgba( #83c373, .3 ); - border-color: #83c373; - } - } - - &.error, - &.invalid { - input[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 { - background: rgba( $color-red, .3 ); - border-color: $color-red; - } - } - - &.llms-visually-hidden-field { - display: none; - } - - &.align-right { - text-align: right; - } - - @media screen and ( min-width: 600px ) { - $i: 1; - @while $i <= 12 { - &.llms-cols-#{$i} { - width: $i / 12 * 100%; - $i: $i + 1; - } - } - } - - &.type-hidden { padding: 0; } - - &.type-radio, - &.type-checkbox { - input, - label { - display: inline; - width: auto; - } - input { - margin-right: 5px; - } - label + .llms-description { - display: block; - } - } - - &.type-radio:not(.is-group) { - - input[type="radio"] { - position: absolute; - opacity: 0; - visibility: none; - } - - label:before { - background: #fafafa; - background-position: -24px 0; - background-repeat: no-repeat; - border-radius: 50%; - box-shadow: hsla( 0,0%,100%,.15) 0 1px 1px, inset hsla(0,0%,0%,.35) 0 0 0 1px; - content: ''; - cursor: pointer; - display: inline-block; - height: 22px; - margin-right: 5px; - position: relative; - transition: background-position .15s cubic-bezier(.8, 0, 1, 1); - top: -3px; - vertical-align: middle; - width: 22px; - z-index: 2; - } - - input[type="radio"]:checked + label:before { - transition: background-position .2s .15s cubic-bezier(0, 0, .2, 1); - background-position: 0 0; - background-image: radial-gradient(ellipse at center, $color-brand-blue 0%,$color-brand-blue 40%, #fafafa 45%); - } - - } - - .llms-input-group { - margin-top: 5px; - .llms-form-field { - padding: 0 0 5px 5px; - } - } - - &.type-reset, - &.type-button, - &.type-submit { - button:not(.auto) { width: 100%; } - } - - .llms-description { - font-size: 14px; - font-style: italic; - } - - .llms-required { - color: $color-red; - margin-left: 4px; - } - - input, textarea, select { - width: 100%; - margin-bottom: 5px; - } - - .select2-container .select2-selection--single { - height: auto; - padding: 4px 6px; - } - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 100%; - } - - } - - - .llms-password-strength-meter { - border: 1px solid #dadada; - display: none; - font-size: 10px; - margin-top: -10px; - padding: 1px; - position: relative; - text-align: center; - - &:before { - bottom: 0; - content: ''; - left: 0; - position: absolute; - top: 0; - transition: width .4s ease; - } - - &.mismatch, - &.too-short, - &.very-weak { - border-color: #e35b5b; - &:before { - background: rgba( #e35b5b, 0.25 ); - width: 25%; - } - } - - &.too-short:before { - width: 0; - } - - &.weak { - border-color: #f78b53; - &:before { - background: rgba( #f78b53, 0.25 ); - width: 50%; - } - } - - &.medium { - border-color: #ffc733; - &:before { - background: rgba( #ffc733, 0.25 ); - width: 75%; - } - } - - &.strong { - border-color: #83c373; - &:before { - background: rgba( #83c373, 0.25 ); - width: 100%; - } - } - } diff --git a/assets/scss/_includes/_mixins.scss b/assets/scss/_includes/_mixins.scss deleted file mode 100644 index 876bb299ff..0000000000 --- a/assets/scss/_includes/_mixins.scss +++ /dev/null @@ -1,120 +0,0 @@ - -@mixin clearfix() { - &:before, - &:after { - content: " "; - display: table; - } - &:after { - clear: both; - } -} - -// -// Positioning mixin -// -// @param [string] $position: position -// @param [list] $args (()): offsets list -// -// @source http://hugogiraudel.com/2013/08/05/offsets-sass-mixin/ -// -@mixin position($position, $args: ()) { - $offsets: top right bottom left; - position: $position; - - @each $offset in $offsets { - $index: index($args, $offset); - - @if $index { - @if $index == length($args) { - #{$offset}: 0; - } - @else { - $next: nth($args, $index + 1); - @if is-valid-length($next) { - #{$offset}: $next; - } - @else if index($offsets, $next) { - #{$offset}: 0; - } - @else { - @warn "Invalid value `#{$next}` for offset `#{$offset}`."; - } - } - } - } -} - -// -// Function checking if $value is a valid length -// @param [literal] $value: value to test -// @return [bool] -// -@function is-valid-length($value) { - $r: (type-of($value) == "number" and not unitless($value)) or (index(auto initial inherit 0, $value) != null); - @return $r; -} - -// -// Shorthands -// -@mixin absolute($args: ()) { - @include position(absolute, $args); -} - -@mixin fixed($args: ()) { - @include position(fixed, $args); -} - -@mixin relative($args: ()) { - @include position(relative, $args); -} - - - -@mixin order_status_badges() { - - .llms-status { - border-radius: 3px; - border-bottom: 1px solid #fff; - display: inline-block; - font-size: 80%; - padding: 3px 6px; - vertical-align: middle; - - &.llms-size--large { - font-size: 105%; - padding: 6px 12px; - } - - &.llms-active, - &.llms-completed, - &.llms-pass, // quiz - &.llms-txn-succeeded { - color: darken( $color-green, 45 ); - background-color: $color-green; - } - - &.llms-fail, // quiz - &.llms-failed, - &.llms-expired, - &.llms-cancelled, - &.llms-txn-failed { - color: darken( $color-red, 40 ); - background-color: $color-red; - } - - &.llms-incomplete, // assignment - &.llms-on-hold, - &.llms-pending, - &.llms-pending-cancel, - &.llms-refunded, - &.llms-txn-pending, - &.llms-txn-refunded { - color: darken( orange, 30 ); - background-color: orange; - } - - } - -} diff --git a/assets/scss/_includes/_quiz-result-question-list.scss b/assets/scss/_includes/_quiz-result-question-list.scss deleted file mode 100644 index fc85ba61b7..0000000000 --- a/assets/scss/_includes/_quiz-result-question-list.scss +++ /dev/null @@ -1,132 +0,0 @@ -.llms-quiz-attempt-results { - margin: 0; - padding: 0; - list-style-type: none; - - .llms-quiz-attempt-question { - background: #efefef; - margin: 0 0 10px; - position: relative; - - .toggle-answer { - @include clearfix(); - color: inherit; - display: block; - padding: 10px 35px 10px 10px; - text-decoration: none; - } - - &.status--waiting.correct, - &.status--waiting.incorrect { - background: rgba( $color-orange, 0.2 ); - .llms-status-icon { - background-color: $color-orange; - } - } - - &.status--graded.correct { - background: rgba( $color-green, 0.2 ); - .llms-status-icon { - background-color: $color-green; - } - } - &.status--graded.incorrect { - background: rgba( $color-red, 0.2 ); - .llms-status-icon { - background-color: $color-red; - } - } - - .llms-question-title { - float: left; - margin: 0; - line-height: 1; - } - - .llms-points { - float: right; - line-height: 1; - } - - .llms-status-icon-tip { - position: absolute; - right: -12px; - top: -2px; - } - - .llms-status-icon { - color: rgba( 255, 255, 255, 0.65 ); - border-radius: 50%; - font-size: 30px; - height: 40px; - line-height: 40px; - text-align: center; - width: 40px; - } - - .llms-quiz-attempt-question-main { - display: none; - padding: 0 10px 10px; - - .llms-quiz-results-label { - font-weight: 700; - margin-bottom: 10px; - } - - ul.llms-quiz-attempt-answers { - margin: 0; - padding: 0; - li.llms-quiz-attempt-answer { - padding: 0; - margin: 0 0 0 30px; - &:only-child { - list-style-type: none; - margin-left: 0; - } - } - } - - img { - height: auto; - max-width: 200px; - } - - .llms-quiz-attempt-answer-section { - border-top: 2px solid rgba( #fff, 0.5 ); - margin-top: 20px; - padding-top: 20px; - &:first-child { - border-top: none; - margin-top: 0; - padding-top: 0; - } - } - - } - - &.type--picture_choice, - &.type--picture_reorder { - ul.llms-quiz-attempt-answers { - list-style-type: none; - margin: 0; - padding: 0; - - li.llms-quiz-attempt-answer { - display: inline-block; - list-style-type: none; - margin: 0; - padding: 5px; - } - } - } - - &.type--removed { - .llms-question-title { - font-style: italic; - font-weight: normal; - } - opacity: .5; - } - - } -} diff --git a/assets/scss/_includes/_spinner.scss b/assets/scss/_includes/_spinner.scss deleted file mode 100644 index 2444d3f075..0000000000 --- a/assets/scss/_includes/_spinner.scss +++ /dev/null @@ -1,43 +0,0 @@ -.llms-spinning { - background: rgba( 250, 250, 250, 0.7 ); - bottom: 0; - display: none; - left: 0; - position: absolute; - right: 0; - top: 0; -} - -.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; - - &.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) - } -} diff --git a/assets/scss/_includes/_tooltip.scss b/assets/scss/_includes/_tooltip.scss deleted file mode 100644 index 39e5f77f15..0000000000 --- a/assets/scss/_includes/_tooltip.scss +++ /dev/null @@ -1,135 +0,0 @@ -.lifterlms, // Settings & Course Builder. -.llms-metabox, // Some Metaboxes. -.llms-mb-container, // Other Metaboxes. -.llms-quiz-wrapper { // Quiz results. - - [data-tip], - [data-title-default], - [data-title-active] { - - $bgcolor: #444; - - position: relative; - - &.tip--top-right { - &:before { - bottom: 100%; - left: -10px; - } - &:hover:before { - bottom: calc( 100% + 6px ); - } - &:after { - border-top-color: $bgcolor; - left: 6px; - top: 0; - } - &:hover:after { - top: -6px; - } - } - - - &.tip--top-left { - &:before { - bottom: 100%; - right: -10px; - } - &:hover:before { - bottom: calc( 100% + 6px ); - } - &:after { - border-top-color: $bgcolor; - right: 6px; - top: 0; - } - &:hover:after { - top: -6px; - } - } - - - - &.tip--bottom-left { - &:before { - top: 100%; - right: -10px; - } - &:hover:before { - top: calc( 100% + 6px ); - } - &:after { - border-bottom-color: $bgcolor; - right: 6px; - bottom: 0; - } - &:hover:after { - bottom: -6px; - } - } - - &.tip--bottom-right { - &:before { - top: 100%; - left: -10px; - } - &:hover:before { - top: calc( 100% + 6px ); - } - &:after { - border-bottom-color: $bgcolor; - left: 6px; - bottom: 0; - } - &:hover:after { - bottom: -6px; - } - } - - &:before { - background: $bgcolor; - border-radius: 4px; - color: #fff; - font-size: 13px; - line-height: 1.2; - padding: 8px; - max-width: 300px; - width: max-content; - } - &:after { - content: ''; - border: 6px solid transparent; - height: 0; - width: 0; - } - - &:before, - &:after { - opacity: 0; - transition: all 0.2s 0.1s ease; - position: absolute; - pointer-events: none; - visibility: hidden; - } - &:hover:before, - &:hover:after { - opacity: 1; - transition: all 0.2s 0.6s ease; - visibility: visible; - z-index: 99999999; - } - - } - - [data-tip] { - &:before { - content: attr(data-tip); - } - } - [data-tip].active { - &:before { - content: attr(data-tip-active); - } - } - -} diff --git a/assets/scss/_includes/_vars-brand-colors.scss b/assets/scss/_includes/_vars-brand-colors.scss deleted file mode 100644 index 9f4660b944..0000000000 --- a/assets/scss/_includes/_vars-brand-colors.scss +++ /dev/null @@ -1,19 +0,0 @@ -// -// LifterLMS Brand Colors -// Currently overrides brand colors on the admin panel -// - -$color-brand-blue: #466dd8; -$color-brand-blue-dark: darken( $color-brand-blue, 8 ); -$color-brand-dark-blue: darken( $color-brand-blue, 24 ); -$color-brand-blue-light: lighten( $color-brand-blue, 8 ); - -$color-brand-orange: #f8954f; -$color-brand-orange-dark: #f67d28; -$color-brand-orange-light: lighten( $color-brand-orange, 8 ); - -$color-brand-aqua: #17bebb; - -$color-brand-pink: #ef476f; - -$color-blue: $color-brand-blue; diff --git a/assets/scss/_includes/_vars.scss b/assets/scss/_includes/_vars.scss deleted file mode 100644 index 57eda78b9f..0000000000 --- a/assets/scss/_includes/_vars.scss +++ /dev/null @@ -1,69 +0,0 @@ -// ----- LifterLMS Brand Colors ----- \\ -$color-brand-dark-blue: #243c56; - -$color-brand-blue: #2295ff; -$color-brand-blue-dark: darken( $color-brand-blue, 12 ); // #0077e4 -$color-brand-blue-light: lighten( $color-brand-blue, 8 ); - -$color-brand-orange: #f8954f; -$color-brand-orange-dark: #f67d28; -$color-brand-orange-light: lighten( $color-brand-orange, 8 ); - -$color-brand-aqua: #17bebb; - -$color-brand-pink: #ef476f; - - - -// ----- name our versions of common colors ----- \\ -$color-black: #010101; -$color-green: #83c373; -$color-blue: $color-brand-blue; -$color-red: #e5554e; -$color-white: #fefefe; -$color-aqua: #35bbaa; -$color-purple: #845ef7; -$color-orange: #ff922b; - -$color-red-hover: darken($color-red,5); - - -// ----- state / action names ----- \\ -$color-success: $color-green; -$color-danger: $color-red; - - - - - - - - -$color-lightgrey: #ccc; -$color-grey: #999; -$color-darkgrey: #666; -$color-cinder: #444; -$color-lightblue: #33b1cb; -$color-darkblue: #0185a3; - - - - - - - - - - - - -$color-border: #efefef; - -$el-box-shadow: 0 1px 2px 0 rgba($color-black,0.4); -$el-background: #f1f1f1; -$el-background-hover: #eaeaea; - -$break-xsmall: 320px; -$break-small: 641px; -$break-medium: 768px; -$break-large: 1024px; diff --git a/assets/scss/_includes/vendor/_font-awesome.scss b/assets/scss/_includes/vendor/_font-awesome.scss deleted file mode 100644 index ee906a8196..0000000000 --- a/assets/scss/_includes/vendor/_font-awesome.scss +++ /dev/null @@ -1,2337 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); - 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'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; -} -.fa-pull-left { - float: left; -} -.fa-pull-right { - float: right; -} -.fa.fa-pull-left { - margin-right: .3em; -} -.fa.fa-pull-right { - margin-left: .3em; -} -/* Deprecated as of 4.4.0 */ -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .3em; -} -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); -} -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - filter: none; -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: "\f000"; -} -.fa-music:before { - content: "\f001"; -} -.fa-search:before { - content: "\f002"; -} -.fa-envelope-o:before { - content: "\f003"; -} -.fa-heart:before { - content: "\f004"; -} -.fa-star:before { - content: "\f005"; -} -.fa-star-o:before { - content: "\f006"; -} -.fa-user:before { - content: "\f007"; -} -.fa-film:before { - content: "\f008"; -} -.fa-th-large:before { - content: "\f009"; -} -.fa-th:before { - content: "\f00a"; -} -.fa-th-list:before { - content: "\f00b"; -} -.fa-check:before { - content: "\f00c"; -} -.fa-remove:before, -.fa-close:before, -.fa-times:before { - content: "\f00d"; -} -.fa-search-plus:before { - content: "\f00e"; -} -.fa-search-minus:before { - content: "\f010"; -} -.fa-power-off:before { - content: "\f011"; -} -.fa-signal:before { - content: "\f012"; -} -.fa-gear:before, -.fa-cog:before { - content: "\f013"; -} -.fa-trash-o:before { - content: "\f014"; -} -.fa-home:before { - content: "\f015"; -} -.fa-file-o:before { - content: "\f016"; -} -.fa-clock-o:before { - content: "\f017"; -} -.fa-road:before { - content: "\f018"; -} -.fa-download:before { - content: "\f019"; -} -.fa-arrow-circle-o-down:before { - content: "\f01a"; -} -.fa-arrow-circle-o-up:before { - content: "\f01b"; -} -.fa-inbox:before { - content: "\f01c"; -} -.fa-play-circle-o:before { - content: "\f01d"; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: "\f01e"; -} -.fa-refresh:before { - content: "\f021"; -} -.fa-list-alt:before { - content: "\f022"; -} -.fa-lock:before { - content: "\f023"; -} -.fa-flag:before { - content: "\f024"; -} -.fa-headphones:before { - content: "\f025"; -} -.fa-volume-off:before { - content: "\f026"; -} -.fa-volume-down:before { - content: "\f027"; -} -.fa-volume-up:before { - content: "\f028"; -} -.fa-qrcode:before { - content: "\f029"; -} -.fa-barcode:before { - content: "\f02a"; -} -.fa-tag:before { - content: "\f02b"; -} -.fa-tags:before { - content: "\f02c"; -} -.fa-book:before { - content: "\f02d"; -} -.fa-bookmark:before { - content: "\f02e"; -} -.fa-print:before { - content: "\f02f"; -} -.fa-camera:before { - content: "\f030"; -} -.fa-font:before { - content: "\f031"; -} -.fa-bold:before { - content: "\f032"; -} -.fa-italic:before { - content: "\f033"; -} -.fa-text-height:before { - content: "\f034"; -} -.fa-text-width:before { - content: "\f035"; -} -.fa-align-left:before { - content: "\f036"; -} -.fa-align-center:before { - content: "\f037"; -} -.fa-align-right:before { - content: "\f038"; -} -.fa-align-justify:before { - content: "\f039"; -} -.fa-list:before { - content: "\f03a"; -} -.fa-dedent:before, -.fa-outdent:before { - content: "\f03b"; -} -.fa-indent:before { - content: "\f03c"; -} -.fa-video-camera:before { - content: "\f03d"; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: "\f03e"; -} -.fa-pencil:before { - content: "\f040"; -} -.fa-map-marker:before { - content: "\f041"; -} -.fa-adjust:before { - content: "\f042"; -} -.fa-tint:before { - content: "\f043"; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: "\f044"; -} -.fa-share-square-o:before { - content: "\f045"; -} -.fa-check-square-o:before { - content: "\f046"; -} -.fa-arrows:before { - content: "\f047"; -} -.fa-step-backward:before { - content: "\f048"; -} -.fa-fast-backward:before { - content: "\f049"; -} -.fa-backward:before { - content: "\f04a"; -} -.fa-play:before { - content: "\f04b"; -} -.fa-pause:before { - content: "\f04c"; -} -.fa-stop:before { - content: "\f04d"; -} -.fa-forward:before { - content: "\f04e"; -} -.fa-fast-forward:before { - content: "\f050"; -} -.fa-step-forward:before { - content: "\f051"; -} -.fa-eject:before { - content: "\f052"; -} -.fa-chevron-left:before { - content: "\f053"; -} -.fa-chevron-right:before { - content: "\f054"; -} -.fa-plus-circle:before { - content: "\f055"; -} -.fa-minus-circle:before { - content: "\f056"; -} -.fa-times-circle:before { - content: "\f057"; -} -.fa-check-circle:before { - content: "\f058"; -} -.fa-question-circle:before { - content: "\f059"; -} -.fa-info-circle:before { - content: "\f05a"; -} -.fa-crosshairs:before { - content: "\f05b"; -} -.fa-times-circle-o:before { - content: "\f05c"; -} -.fa-check-circle-o:before { - content: "\f05d"; -} -.fa-ban:before { - content: "\f05e"; -} -.fa-arrow-left:before { - content: "\f060"; -} -.fa-arrow-right:before { - content: "\f061"; -} -.fa-arrow-up:before { - content: "\f062"; -} -.fa-arrow-down:before { - content: "\f063"; -} -.fa-mail-forward:before, -.fa-share:before { - content: "\f064"; -} -.fa-expand:before { - content: "\f065"; -} -.fa-compress:before { - content: "\f066"; -} -.fa-plus:before { - content: "\f067"; -} -.fa-minus:before { - content: "\f068"; -} -.fa-asterisk:before { - content: "\f069"; -} -.fa-exclamation-circle:before { - content: "\f06a"; -} -.fa-gift:before { - content: "\f06b"; -} -.fa-leaf:before { - content: "\f06c"; -} -.fa-fire:before { - content: "\f06d"; -} -.fa-eye:before { - content: "\f06e"; -} -.fa-eye-slash:before { - content: "\f070"; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: "\f071"; -} -.fa-plane:before { - content: "\f072"; -} -.fa-calendar:before { - content: "\f073"; -} -.fa-random:before { - content: "\f074"; -} -.fa-comment:before { - content: "\f075"; -} -.fa-magnet:before { - content: "\f076"; -} -.fa-chevron-up:before { - content: "\f077"; -} -.fa-chevron-down:before { - content: "\f078"; -} -.fa-retweet:before { - content: "\f079"; -} -.fa-shopping-cart:before { - content: "\f07a"; -} -.fa-folder:before { - content: "\f07b"; -} -.fa-folder-open:before { - content: "\f07c"; -} -.fa-arrows-v:before { - content: "\f07d"; -} -.fa-arrows-h:before { - content: "\f07e"; -} -.fa-bar-chart-o:before, -.fa-bar-chart:before { - content: "\f080"; -} -.fa-twitter-square:before { - content: "\f081"; -} -.fa-facebook-square:before { - content: "\f082"; -} -.fa-camera-retro:before { - content: "\f083"; -} -.fa-key:before { - content: "\f084"; -} -.fa-gears:before, -.fa-cogs:before { - content: "\f085"; -} -.fa-comments:before { - content: "\f086"; -} -.fa-thumbs-o-up:before { - content: "\f087"; -} -.fa-thumbs-o-down:before { - content: "\f088"; -} -.fa-star-half:before { - content: "\f089"; -} -.fa-heart-o:before { - content: "\f08a"; -} -.fa-sign-out:before { - content: "\f08b"; -} -.fa-linkedin-square:before { - content: "\f08c"; -} -.fa-thumb-tack:before { - content: "\f08d"; -} -.fa-external-link:before { - content: "\f08e"; -} -.fa-sign-in:before { - content: "\f090"; -} -.fa-trophy:before { - content: "\f091"; -} -.fa-github-square:before { - content: "\f092"; -} -.fa-upload:before { - content: "\f093"; -} -.fa-lemon-o:before { - content: "\f094"; -} -.fa-phone:before { - content: "\f095"; -} -.fa-square-o:before { - content: "\f096"; -} -.fa-bookmark-o:before { - content: "\f097"; -} -.fa-phone-square:before { - content: "\f098"; -} -.fa-twitter:before { - content: "\f099"; -} -.fa-facebook-f:before, -.fa-facebook:before { - content: "\f09a"; -} -.fa-github:before { - content: "\f09b"; -} -.fa-unlock:before { - content: "\f09c"; -} -.fa-credit-card:before { - content: "\f09d"; -} -.fa-feed:before, -.fa-rss:before { - content: "\f09e"; -} -.fa-hdd-o:before { - content: "\f0a0"; -} -.fa-bullhorn:before { - content: "\f0a1"; -} -.fa-bell:before { - content: "\f0f3"; -} -.fa-certificate:before { - content: "\f0a3"; -} -.fa-hand-o-right:before { - content: "\f0a4"; -} -.fa-hand-o-left:before { - content: "\f0a5"; -} -.fa-hand-o-up:before { - content: "\f0a6"; -} -.fa-hand-o-down:before { - content: "\f0a7"; -} -.fa-arrow-circle-left:before { - content: "\f0a8"; -} -.fa-arrow-circle-right:before { - content: "\f0a9"; -} -.fa-arrow-circle-up:before { - content: "\f0aa"; -} -.fa-arrow-circle-down:before { - content: "\f0ab"; -} -.fa-globe:before { - content: "\f0ac"; -} -.fa-wrench:before { - content: "\f0ad"; -} -.fa-tasks:before { - content: "\f0ae"; -} -.fa-filter:before { - content: "\f0b0"; -} -.fa-briefcase:before { - content: "\f0b1"; -} -.fa-arrows-alt:before { - content: "\f0b2"; -} -.fa-group:before, -.fa-users:before { - content: "\f0c0"; -} -.fa-chain:before, -.fa-link:before { - content: "\f0c1"; -} -.fa-cloud:before { - content: "\f0c2"; -} -.fa-flask:before { - content: "\f0c3"; -} -.fa-cut:before, -.fa-scissors:before { - content: "\f0c4"; -} -.fa-copy:before, -.fa-files-o:before { - content: "\f0c5"; -} -.fa-paperclip:before { - content: "\f0c6"; -} -.fa-save:before, -.fa-floppy-o:before { - content: "\f0c7"; -} -.fa-square:before { - content: "\f0c8"; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: "\f0c9"; -} -.fa-list-ul:before { - content: "\f0ca"; -} -.fa-list-ol:before { - content: "\f0cb"; -} -.fa-strikethrough:before { - content: "\f0cc"; -} -.fa-underline:before { - content: "\f0cd"; -} -.fa-table:before { - content: "\f0ce"; -} -.fa-magic:before { - content: "\f0d0"; -} -.fa-truck:before { - content: "\f0d1"; -} -.fa-pinterest:before { - content: "\f0d2"; -} -.fa-pinterest-square:before { - content: "\f0d3"; -} -.fa-google-plus-square:before { - content: "\f0d4"; -} -.fa-google-plus:before { - content: "\f0d5"; -} -.fa-money:before { - content: "\f0d6"; -} -.fa-caret-down:before { - content: "\f0d7"; -} -.fa-caret-up:before { - content: "\f0d8"; -} -.fa-caret-left:before { - content: "\f0d9"; -} -.fa-caret-right:before { - content: "\f0da"; -} -.fa-columns:before { - content: "\f0db"; -} -.fa-unsorted:before, -.fa-sort:before { - content: "\f0dc"; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: "\f0dd"; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: "\f0de"; -} -.fa-envelope:before { - content: "\f0e0"; -} -.fa-linkedin:before { - content: "\f0e1"; -} -.fa-rotate-left:before, -.fa-undo:before { - content: "\f0e2"; -} -.fa-legal:before, -.fa-gavel:before { - content: "\f0e3"; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: "\f0e4"; -} -.fa-comment-o:before { - content: "\f0e5"; -} -.fa-comments-o:before { - content: "\f0e6"; -} -.fa-flash:before, -.fa-bolt:before { - content: "\f0e7"; -} -.fa-sitemap:before { - content: "\f0e8"; -} -.fa-umbrella:before { - content: "\f0e9"; -} -.fa-paste:before, -.fa-clipboard:before { - content: "\f0ea"; -} -.fa-lightbulb-o:before { - content: "\f0eb"; -} -.fa-exchange:before { - content: "\f0ec"; -} -.fa-cloud-download:before { - content: "\f0ed"; -} -.fa-cloud-upload:before { - content: "\f0ee"; -} -.fa-user-md:before { - content: "\f0f0"; -} -.fa-stethoscope:before { - content: "\f0f1"; -} -.fa-suitcase:before { - content: "\f0f2"; -} -.fa-bell-o:before { - content: "\f0a2"; -} -.fa-coffee:before { - content: "\f0f4"; -} -.fa-cutlery:before { - content: "\f0f5"; -} -.fa-file-text-o:before { - content: "\f0f6"; -} -.fa-building-o:before { - content: "\f0f7"; -} -.fa-hospital-o:before { - content: "\f0f8"; -} -.fa-ambulance:before { - content: "\f0f9"; -} -.fa-medkit:before { - content: "\f0fa"; -} -.fa-fighter-jet:before { - content: "\f0fb"; -} -.fa-beer:before { - content: "\f0fc"; -} -.fa-h-square:before { - content: "\f0fd"; -} -.fa-plus-square:before { - content: "\f0fe"; -} -.fa-angle-double-left:before { - content: "\f100"; -} -.fa-angle-double-right:before { - content: "\f101"; -} -.fa-angle-double-up:before { - content: "\f102"; -} -.fa-angle-double-down:before { - content: "\f103"; -} -.fa-angle-left:before { - content: "\f104"; -} -.fa-angle-right:before { - content: "\f105"; -} -.fa-angle-up:before { - content: "\f106"; -} -.fa-angle-down:before { - content: "\f107"; -} -.fa-desktop:before { - content: "\f108"; -} -.fa-laptop:before { - content: "\f109"; -} -.fa-tablet:before { - content: "\f10a"; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: "\f10b"; -} -.fa-circle-o:before { - content: "\f10c"; -} -.fa-quote-left:before { - content: "\f10d"; -} -.fa-quote-right:before { - content: "\f10e"; -} -.fa-spinner:before { - content: "\f110"; -} -.fa-circle:before { - content: "\f111"; -} -.fa-mail-reply:before, -.fa-reply:before { - content: "\f112"; -} -.fa-github-alt:before { - content: "\f113"; -} -.fa-folder-o:before { - content: "\f114"; -} -.fa-folder-open-o:before { - content: "\f115"; -} -.fa-smile-o:before { - content: "\f118"; -} -.fa-frown-o:before { - content: "\f119"; -} -.fa-meh-o:before { - content: "\f11a"; -} -.fa-gamepad:before { - content: "\f11b"; -} -.fa-keyboard-o:before { - content: "\f11c"; -} -.fa-flag-o:before { - content: "\f11d"; -} -.fa-flag-checkered:before { - content: "\f11e"; -} -.fa-terminal:before { - content: "\f120"; -} -.fa-code:before { - content: "\f121"; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: "\f122"; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: "\f123"; -} -.fa-location-arrow:before { - content: "\f124"; -} -.fa-crop:before { - content: "\f125"; -} -.fa-code-fork:before { - content: "\f126"; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: "\f127"; -} -.fa-question:before { - content: "\f128"; -} -.fa-info:before { - content: "\f129"; -} -.fa-exclamation:before { - content: "\f12a"; -} -.fa-superscript:before { - content: "\f12b"; -} -.fa-subscript:before { - content: "\f12c"; -} -.fa-eraser:before { - content: "\f12d"; -} -.fa-puzzle-piece:before { - content: "\f12e"; -} -.fa-microphone:before { - content: "\f130"; -} -.fa-microphone-slash:before { - content: "\f131"; -} -.fa-shield:before { - content: "\f132"; -} -.fa-calendar-o:before { - content: "\f133"; -} -.fa-fire-extinguisher:before { - content: "\f134"; -} -.fa-rocket:before { - content: "\f135"; -} -.fa-maxcdn:before { - content: "\f136"; -} -.fa-chevron-circle-left:before { - content: "\f137"; -} -.fa-chevron-circle-right:before { - content: "\f138"; -} -.fa-chevron-circle-up:before { - content: "\f139"; -} -.fa-chevron-circle-down:before { - content: "\f13a"; -} -.fa-html5:before { - content: "\f13b"; -} -.fa-css3:before { - content: "\f13c"; -} -.fa-anchor:before { - content: "\f13d"; -} -.fa-unlock-alt:before { - content: "\f13e"; -} -.fa-bullseye:before { - content: "\f140"; -} -.fa-ellipsis-h:before { - content: "\f141"; -} -.fa-ellipsis-v:before { - content: "\f142"; -} -.fa-rss-square:before { - content: "\f143"; -} -.fa-play-circle:before { - content: "\f144"; -} -.fa-ticket:before { - content: "\f145"; -} -.fa-minus-square:before { - content: "\f146"; -} -.fa-minus-square-o:before { - content: "\f147"; -} -.fa-level-up:before { - content: "\f148"; -} -.fa-level-down:before { - content: "\f149"; -} -.fa-check-square:before { - content: "\f14a"; -} -.fa-pencil-square:before { - content: "\f14b"; -} -.fa-external-link-square:before { - content: "\f14c"; -} -.fa-share-square:before { - content: "\f14d"; -} -.fa-compass:before { - content: "\f14e"; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: "\f150"; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: "\f151"; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: "\f152"; -} -.fa-euro:before, -.fa-eur:before { - content: "\f153"; -} -.fa-gbp:before { - content: "\f154"; -} -.fa-dollar:before, -.fa-usd:before { - content: "\f155"; -} -.fa-rupee:before, -.fa-inr:before { - content: "\f156"; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: "\f157"; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: "\f158"; -} -.fa-won:before, -.fa-krw:before { - content: "\f159"; -} -.fa-bitcoin:before, -.fa-btc:before { - content: "\f15a"; -} -.fa-file:before { - content: "\f15b"; -} -.fa-file-text:before { - content: "\f15c"; -} -.fa-sort-alpha-asc:before { - content: "\f15d"; -} -.fa-sort-alpha-desc:before { - content: "\f15e"; -} -.fa-sort-amount-asc:before { - content: "\f160"; -} -.fa-sort-amount-desc:before { - content: "\f161"; -} -.fa-sort-numeric-asc:before { - content: "\f162"; -} -.fa-sort-numeric-desc:before { - content: "\f163"; -} -.fa-thumbs-up:before { - content: "\f164"; -} -.fa-thumbs-down:before { - content: "\f165"; -} -.fa-youtube-square:before { - content: "\f166"; -} -.fa-youtube:before { - content: "\f167"; -} -.fa-xing:before { - content: "\f168"; -} -.fa-xing-square:before { - content: "\f169"; -} -.fa-youtube-play:before { - content: "\f16a"; -} -.fa-dropbox:before { - content: "\f16b"; -} -.fa-stack-overflow:before { - content: "\f16c"; -} -.fa-instagram:before { - content: "\f16d"; -} -.fa-flickr:before { - content: "\f16e"; -} -.fa-adn:before { - content: "\f170"; -} -.fa-bitbucket:before { - content: "\f171"; -} -.fa-bitbucket-square:before { - content: "\f172"; -} -.fa-tumblr:before { - content: "\f173"; -} -.fa-tumblr-square:before { - content: "\f174"; -} -.fa-long-arrow-down:before { - content: "\f175"; -} -.fa-long-arrow-up:before { - content: "\f176"; -} -.fa-long-arrow-left:before { - content: "\f177"; -} -.fa-long-arrow-right:before { - content: "\f178"; -} -.fa-apple:before { - content: "\f179"; -} -.fa-windows:before { - content: "\f17a"; -} -.fa-android:before { - content: "\f17b"; -} -.fa-linux:before { - content: "\f17c"; -} -.fa-dribbble:before { - content: "\f17d"; -} -.fa-skype:before { - content: "\f17e"; -} -.fa-foursquare:before { - content: "\f180"; -} -.fa-trello:before { - content: "\f181"; -} -.fa-female:before { - content: "\f182"; -} -.fa-male:before { - content: "\f183"; -} -.fa-gittip:before, -.fa-gratipay:before { - content: "\f184"; -} -.fa-sun-o:before { - content: "\f185"; -} -.fa-moon-o:before { - content: "\f186"; -} -.fa-archive:before { - content: "\f187"; -} -.fa-bug:before { - content: "\f188"; -} -.fa-vk:before { - content: "\f189"; -} -.fa-weibo:before { - content: "\f18a"; -} -.fa-renren:before { - content: "\f18b"; -} -.fa-pagelines:before { - content: "\f18c"; -} -.fa-stack-exchange:before { - content: "\f18d"; -} -.fa-arrow-circle-o-right:before { - content: "\f18e"; -} -.fa-arrow-circle-o-left:before { - content: "\f190"; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: "\f191"; -} -.fa-dot-circle-o:before { - content: "\f192"; -} -.fa-wheelchair:before { - content: "\f193"; -} -.fa-vimeo-square:before { - content: "\f194"; -} -.fa-turkish-lira:before, -.fa-try:before { - content: "\f195"; -} -.fa-plus-square-o:before { - content: "\f196"; -} -.fa-space-shuttle:before { - content: "\f197"; -} -.fa-slack:before { - content: "\f198"; -} -.fa-envelope-square:before { - content: "\f199"; -} -.fa-wordpress:before { - content: "\f19a"; -} -.fa-openid:before { - content: "\f19b"; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: "\f19c"; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: "\f19d"; -} -.fa-yahoo:before { - content: "\f19e"; -} -.fa-google:before { - content: "\f1a0"; -} -.fa-reddit:before { - content: "\f1a1"; -} -.fa-reddit-square:before { - content: "\f1a2"; -} -.fa-stumbleupon-circle:before { - content: "\f1a3"; -} -.fa-stumbleupon:before { - content: "\f1a4"; -} -.fa-delicious:before { - content: "\f1a5"; -} -.fa-digg:before { - content: "\f1a6"; -} -.fa-pied-piper-pp:before { - content: "\f1a7"; -} -.fa-pied-piper-alt:before { - content: "\f1a8"; -} -.fa-drupal:before { - content: "\f1a9"; -} -.fa-joomla:before { - content: "\f1aa"; -} -.fa-language:before { - content: "\f1ab"; -} -.fa-fax:before { - content: "\f1ac"; -} -.fa-building:before { - content: "\f1ad"; -} -.fa-child:before { - content: "\f1ae"; -} -.fa-paw:before { - content: "\f1b0"; -} -.fa-spoon:before { - content: "\f1b1"; -} -.fa-cube:before { - content: "\f1b2"; -} -.fa-cubes:before { - content: "\f1b3"; -} -.fa-behance:before { - content: "\f1b4"; -} -.fa-behance-square:before { - content: "\f1b5"; -} -.fa-steam:before { - content: "\f1b6"; -} -.fa-steam-square:before { - content: "\f1b7"; -} -.fa-recycle:before { - content: "\f1b8"; -} -.fa-automobile:before, -.fa-car:before { - content: "\f1b9"; -} -.fa-cab:before, -.fa-taxi:before { - content: "\f1ba"; -} -.fa-tree:before { - content: "\f1bb"; -} -.fa-spotify:before { - content: "\f1bc"; -} -.fa-deviantart:before { - content: "\f1bd"; -} -.fa-soundcloud:before { - content: "\f1be"; -} -.fa-database:before { - content: "\f1c0"; -} -.fa-file-pdf-o:before { - content: "\f1c1"; -} -.fa-file-word-o:before { - content: "\f1c2"; -} -.fa-file-excel-o:before { - content: "\f1c3"; -} -.fa-file-powerpoint-o:before { - content: "\f1c4"; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: "\f1c5"; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: "\f1c6"; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: "\f1c7"; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: "\f1c8"; -} -.fa-file-code-o:before { - content: "\f1c9"; -} -.fa-vine:before { - content: "\f1ca"; -} -.fa-codepen:before { - content: "\f1cb"; -} -.fa-jsfiddle:before { - content: "\f1cc"; -} -.fa-life-bouy:before, -.fa-life-buoy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: "\f1cd"; -} -.fa-circle-o-notch:before { - content: "\f1ce"; -} -.fa-ra:before, -.fa-resistance:before, -.fa-rebel:before { - content: "\f1d0"; -} -.fa-ge:before, -.fa-empire:before { - content: "\f1d1"; -} -.fa-git-square:before { - content: "\f1d2"; -} -.fa-git:before { - content: "\f1d3"; -} -.fa-y-combinator-square:before, -.fa-yc-square:before, -.fa-hacker-news:before { - content: "\f1d4"; -} -.fa-tencent-weibo:before { - content: "\f1d5"; -} -.fa-qq:before { - content: "\f1d6"; -} -.fa-wechat:before, -.fa-weixin:before { - content: "\f1d7"; -} -.fa-send:before, -.fa-paper-plane:before { - content: "\f1d8"; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: "\f1d9"; -} -.fa-history:before { - content: "\f1da"; -} -.fa-circle-thin:before { - content: "\f1db"; -} -.fa-header:before { - content: "\f1dc"; -} -.fa-paragraph:before { - content: "\f1dd"; -} -.fa-sliders:before { - content: "\f1de"; -} -.fa-share-alt:before { - content: "\f1e0"; -} -.fa-share-alt-square:before { - content: "\f1e1"; -} -.fa-bomb:before { - content: "\f1e2"; -} -.fa-soccer-ball-o:before, -.fa-futbol-o:before { - content: "\f1e3"; -} -.fa-tty:before { - content: "\f1e4"; -} -.fa-binoculars:before { - content: "\f1e5"; -} -.fa-plug:before { - content: "\f1e6"; -} -.fa-slideshare:before { - content: "\f1e7"; -} -.fa-twitch:before { - content: "\f1e8"; -} -.fa-yelp:before { - content: "\f1e9"; -} -.fa-newspaper-o:before { - content: "\f1ea"; -} -.fa-wifi:before { - content: "\f1eb"; -} -.fa-calculator:before { - content: "\f1ec"; -} -.fa-paypal:before { - content: "\f1ed"; -} -.fa-google-wallet:before { - content: "\f1ee"; -} -.fa-cc-visa:before { - content: "\f1f0"; -} -.fa-cc-mastercard:before { - content: "\f1f1"; -} -.fa-cc-discover:before { - content: "\f1f2"; -} -.fa-cc-amex:before { - content: "\f1f3"; -} -.fa-cc-paypal:before { - content: "\f1f4"; -} -.fa-cc-stripe:before { - content: "\f1f5"; -} -.fa-bell-slash:before { - content: "\f1f6"; -} -.fa-bell-slash-o:before { - content: "\f1f7"; -} -.fa-trash:before { - content: "\f1f8"; -} -.fa-copyright:before { - content: "\f1f9"; -} -.fa-at:before { - content: "\f1fa"; -} -.fa-eyedropper:before { - content: "\f1fb"; -} -.fa-paint-brush:before { - content: "\f1fc"; -} -.fa-birthday-cake:before { - content: "\f1fd"; -} -.fa-area-chart:before { - content: "\f1fe"; -} -.fa-pie-chart:before { - content: "\f200"; -} -.fa-line-chart:before { - content: "\f201"; -} -.fa-lastfm:before { - content: "\f202"; -} -.fa-lastfm-square:before { - content: "\f203"; -} -.fa-toggle-off:before { - content: "\f204"; -} -.fa-toggle-on:before { - content: "\f205"; -} -.fa-bicycle:before { - content: "\f206"; -} -.fa-bus:before { - content: "\f207"; -} -.fa-ioxhost:before { - content: "\f208"; -} -.fa-angellist:before { - content: "\f209"; -} -.fa-cc:before { - content: "\f20a"; -} -.fa-shekel:before, -.fa-sheqel:before, -.fa-ils:before { - content: "\f20b"; -} -.fa-meanpath:before { - content: "\f20c"; -} -.fa-buysellads:before { - content: "\f20d"; -} -.fa-connectdevelop:before { - content: "\f20e"; -} -.fa-dashcube:before { - content: "\f210"; -} -.fa-forumbee:before { - content: "\f211"; -} -.fa-leanpub:before { - content: "\f212"; -} -.fa-sellsy:before { - content: "\f213"; -} -.fa-shirtsinbulk:before { - content: "\f214"; -} -.fa-simplybuilt:before { - content: "\f215"; -} -.fa-skyatlas:before { - content: "\f216"; -} -.fa-cart-plus:before { - content: "\f217"; -} -.fa-cart-arrow-down:before { - content: "\f218"; -} -.fa-diamond:before { - content: "\f219"; -} -.fa-ship:before { - content: "\f21a"; -} -.fa-user-secret:before { - content: "\f21b"; -} -.fa-motorcycle:before { - content: "\f21c"; -} -.fa-street-view:before { - content: "\f21d"; -} -.fa-heartbeat:before { - content: "\f21e"; -} -.fa-venus:before { - content: "\f221"; -} -.fa-mars:before { - content: "\f222"; -} -.fa-mercury:before { - content: "\f223"; -} -.fa-intersex:before, -.fa-transgender:before { - content: "\f224"; -} -.fa-transgender-alt:before { - content: "\f225"; -} -.fa-venus-double:before { - content: "\f226"; -} -.fa-mars-double:before { - content: "\f227"; -} -.fa-venus-mars:before { - content: "\f228"; -} -.fa-mars-stroke:before { - content: "\f229"; -} -.fa-mars-stroke-v:before { - content: "\f22a"; -} -.fa-mars-stroke-h:before { - content: "\f22b"; -} -.fa-neuter:before { - content: "\f22c"; -} -.fa-genderless:before { - content: "\f22d"; -} -.fa-facebook-official:before { - content: "\f230"; -} -.fa-pinterest-p:before { - content: "\f231"; -} -.fa-whatsapp:before { - content: "\f232"; -} -.fa-server:before { - content: "\f233"; -} -.fa-user-plus:before { - content: "\f234"; -} -.fa-user-times:before { - content: "\f235"; -} -.fa-hotel:before, -.fa-bed:before { - content: "\f236"; -} -.fa-viacoin:before { - content: "\f237"; -} -.fa-train:before { - content: "\f238"; -} -.fa-subway:before { - content: "\f239"; -} -.fa-medium:before { - content: "\f23a"; -} -.fa-yc:before, -.fa-y-combinator:before { - content: "\f23b"; -} -.fa-optin-monster:before { - content: "\f23c"; -} -.fa-opencart:before { - content: "\f23d"; -} -.fa-expeditedssl:before { - content: "\f23e"; -} -.fa-battery-4:before, -.fa-battery:before, -.fa-battery-full:before { - content: "\f240"; -} -.fa-battery-3:before, -.fa-battery-three-quarters:before { - content: "\f241"; -} -.fa-battery-2:before, -.fa-battery-half:before { - content: "\f242"; -} -.fa-battery-1:before, -.fa-battery-quarter:before { - content: "\f243"; -} -.fa-battery-0:before, -.fa-battery-empty:before { - content: "\f244"; -} -.fa-mouse-pointer:before { - content: "\f245"; -} -.fa-i-cursor:before { - content: "\f246"; -} -.fa-object-group:before { - content: "\f247"; -} -.fa-object-ungroup:before { - content: "\f248"; -} -.fa-sticky-note:before { - content: "\f249"; -} -.fa-sticky-note-o:before { - content: "\f24a"; -} -.fa-cc-jcb:before { - content: "\f24b"; -} -.fa-cc-diners-club:before { - content: "\f24c"; -} -.fa-clone:before { - content: "\f24d"; -} -.fa-balance-scale:before { - content: "\f24e"; -} -.fa-hourglass-o:before { - content: "\f250"; -} -.fa-hourglass-1:before, -.fa-hourglass-start:before { - content: "\f251"; -} -.fa-hourglass-2:before, -.fa-hourglass-half:before { - content: "\f252"; -} -.fa-hourglass-3:before, -.fa-hourglass-end:before { - content: "\f253"; -} -.fa-hourglass:before { - content: "\f254"; -} -.fa-hand-grab-o:before, -.fa-hand-rock-o:before { - content: "\f255"; -} -.fa-hand-stop-o:before, -.fa-hand-paper-o:before { - content: "\f256"; -} -.fa-hand-scissors-o:before { - content: "\f257"; -} -.fa-hand-lizard-o:before { - content: "\f258"; -} -.fa-hand-spock-o:before { - content: "\f259"; -} -.fa-hand-pointer-o:before { - content: "\f25a"; -} -.fa-hand-peace-o:before { - content: "\f25b"; -} -.fa-trademark:before { - content: "\f25c"; -} -.fa-registered:before { - content: "\f25d"; -} -.fa-creative-commons:before { - content: "\f25e"; -} -.fa-gg:before { - content: "\f260"; -} -.fa-gg-circle:before { - content: "\f261"; -} -.fa-tripadvisor:before { - content: "\f262"; -} -.fa-odnoklassniki:before { - content: "\f263"; -} -.fa-odnoklassniki-square:before { - content: "\f264"; -} -.fa-get-pocket:before { - content: "\f265"; -} -.fa-wikipedia-w:before { - content: "\f266"; -} -.fa-safari:before { - content: "\f267"; -} -.fa-chrome:before { - content: "\f268"; -} -.fa-firefox:before { - content: "\f269"; -} -.fa-opera:before { - content: "\f26a"; -} -.fa-internet-explorer:before { - content: "\f26b"; -} -.fa-tv:before, -.fa-television:before { - content: "\f26c"; -} -.fa-contao:before { - content: "\f26d"; -} -.fa-500px:before { - content: "\f26e"; -} -.fa-amazon:before { - content: "\f270"; -} -.fa-calendar-plus-o:before { - content: "\f271"; -} -.fa-calendar-minus-o:before { - content: "\f272"; -} -.fa-calendar-times-o:before { - content: "\f273"; -} -.fa-calendar-check-o:before { - content: "\f274"; -} -.fa-industry:before { - content: "\f275"; -} -.fa-map-pin:before { - content: "\f276"; -} -.fa-map-signs:before { - content: "\f277"; -} -.fa-map-o:before { - content: "\f278"; -} -.fa-map:before { - content: "\f279"; -} -.fa-commenting:before { - content: "\f27a"; -} -.fa-commenting-o:before { - content: "\f27b"; -} -.fa-houzz:before { - content: "\f27c"; -} -.fa-vimeo:before { - content: "\f27d"; -} -.fa-black-tie:before { - content: "\f27e"; -} -.fa-fonticons:before { - content: "\f280"; -} -.fa-reddit-alien:before { - content: "\f281"; -} -.fa-edge:before { - content: "\f282"; -} -.fa-credit-card-alt:before { - content: "\f283"; -} -.fa-codiepie:before { - content: "\f284"; -} -.fa-modx:before { - content: "\f285"; -} -.fa-fort-awesome:before { - content: "\f286"; -} -.fa-usb:before { - content: "\f287"; -} -.fa-product-hunt:before { - content: "\f288"; -} -.fa-mixcloud:before { - content: "\f289"; -} -.fa-scribd:before { - content: "\f28a"; -} -.fa-pause-circle:before { - content: "\f28b"; -} -.fa-pause-circle-o:before { - content: "\f28c"; -} -.fa-stop-circle:before { - content: "\f28d"; -} -.fa-stop-circle-o:before { - content: "\f28e"; -} -.fa-shopping-bag:before { - content: "\f290"; -} -.fa-shopping-basket:before { - content: "\f291"; -} -.fa-hashtag:before { - content: "\f292"; -} -.fa-bluetooth:before { - content: "\f293"; -} -.fa-bluetooth-b:before { - content: "\f294"; -} -.fa-percent:before { - content: "\f295"; -} -.fa-gitlab:before { - content: "\f296"; -} -.fa-wpbeginner:before { - content: "\f297"; -} -.fa-wpforms:before { - content: "\f298"; -} -.fa-envira:before { - content: "\f299"; -} -.fa-universal-access:before { - content: "\f29a"; -} -.fa-wheelchair-alt:before { - content: "\f29b"; -} -.fa-question-circle-o:before { - content: "\f29c"; -} -.fa-blind:before { - content: "\f29d"; -} -.fa-audio-description:before { - content: "\f29e"; -} -.fa-volume-control-phone:before { - content: "\f2a0"; -} -.fa-braille:before { - content: "\f2a1"; -} -.fa-assistive-listening-systems:before { - content: "\f2a2"; -} -.fa-asl-interpreting:before, -.fa-american-sign-language-interpreting:before { - content: "\f2a3"; -} -.fa-deafness:before, -.fa-hard-of-hearing:before, -.fa-deaf:before { - content: "\f2a4"; -} -.fa-glide:before { - content: "\f2a5"; -} -.fa-glide-g:before { - content: "\f2a6"; -} -.fa-signing:before, -.fa-sign-language:before { - content: "\f2a7"; -} -.fa-low-vision:before { - content: "\f2a8"; -} -.fa-viadeo:before { - content: "\f2a9"; -} -.fa-viadeo-square:before { - content: "\f2aa"; -} -.fa-snapchat:before { - content: "\f2ab"; -} -.fa-snapchat-ghost:before { - content: "\f2ac"; -} -.fa-snapchat-square:before { - content: "\f2ad"; -} -.fa-pied-piper:before { - content: "\f2ae"; -} -.fa-first-order:before { - content: "\f2b0"; -} -.fa-yoast:before { - content: "\f2b1"; -} -.fa-themeisle:before { - content: "\f2b2"; -} -.fa-google-plus-circle:before, -.fa-google-plus-official:before { - content: "\f2b3"; -} -.fa-fa:before, -.fa-font-awesome:before { - content: "\f2b4"; -} -.fa-handshake-o:before { - content: "\f2b5"; -} -.fa-envelope-open:before { - content: "\f2b6"; -} -.fa-envelope-open-o:before { - content: "\f2b7"; -} -.fa-linode:before { - content: "\f2b8"; -} -.fa-address-book:before { - content: "\f2b9"; -} -.fa-address-book-o:before { - content: "\f2ba"; -} -.fa-vcard:before, -.fa-address-card:before { - content: "\f2bb"; -} -.fa-vcard-o:before, -.fa-address-card-o:before { - content: "\f2bc"; -} -.fa-user-circle:before { - content: "\f2bd"; -} -.fa-user-circle-o:before { - content: "\f2be"; -} -.fa-user-o:before { - content: "\f2c0"; -} -.fa-id-badge:before { - content: "\f2c1"; -} -.fa-drivers-license:before, -.fa-id-card:before { - content: "\f2c2"; -} -.fa-drivers-license-o:before, -.fa-id-card-o:before { - content: "\f2c3"; -} -.fa-quora:before { - content: "\f2c4"; -} -.fa-free-code-camp:before { - content: "\f2c5"; -} -.fa-telegram:before { - content: "\f2c6"; -} -.fa-thermometer-4:before, -.fa-thermometer:before, -.fa-thermometer-full:before { - content: "\f2c7"; -} -.fa-thermometer-3:before, -.fa-thermometer-three-quarters:before { - content: "\f2c8"; -} -.fa-thermometer-2:before, -.fa-thermometer-half:before { - content: "\f2c9"; -} -.fa-thermometer-1:before, -.fa-thermometer-quarter:before { - content: "\f2ca"; -} -.fa-thermometer-0:before, -.fa-thermometer-empty:before { - content: "\f2cb"; -} -.fa-shower:before { - content: "\f2cc"; -} -.fa-bathtub:before, -.fa-s15:before, -.fa-bath:before { - content: "\f2cd"; -} -.fa-podcast:before { - content: "\f2ce"; -} -.fa-window-maximize:before { - content: "\f2d0"; -} -.fa-window-minimize:before { - content: "\f2d1"; -} -.fa-window-restore:before { - content: "\f2d2"; -} -.fa-times-rectangle:before, -.fa-window-close:before { - content: "\f2d3"; -} -.fa-times-rectangle-o:before, -.fa-window-close-o:before { - content: "\f2d4"; -} -.fa-bandcamp:before { - content: "\f2d5"; -} -.fa-grav:before { - content: "\f2d6"; -} -.fa-etsy:before { - content: "\f2d7"; -} -.fa-imdb:before { - content: "\f2d8"; -} -.fa-ravelry:before { - content: "\f2d9"; -} -.fa-eercast:before { - content: "\f2da"; -} -.fa-microchip:before { - content: "\f2db"; -} -.fa-snowflake-o:before { - content: "\f2dc"; -} -.fa-superpowers:before { - content: "\f2dd"; -} -.fa-wpexplorer:before { - content: "\f2de"; -} -.fa-meetup:before { - content: "\f2e0"; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} diff --git a/assets/scss/admin-importer.scss b/assets/scss/admin-importer.scss deleted file mode 100644 index f06b43c3ef..0000000000 --- a/assets/scss/admin-importer.scss +++ /dev/null @@ -1,71 +0,0 @@ -@import "_includes/vars"; -@import "_includes/mixins"; - -.llms-import-file-wrap { - background: #fafafa; - border: 1px solid #ccd0d4; - padding: 10px; - margin: 20px auto; - display: inline-flex; - justify-content: space-between; - align-items: center; -} - -.llms-cloud-import-help.button-link { - color: inherit; - vertical-align: top; - text-decoration: none; -} - -ul.llms-importable-courses { - margin: 0 -10px; - li.llms-importable-course { - background: #fff; - margin: 10px; - img { - display: block; - max-width: 100%; - } - h3 { - margin: 20px 20px 10px; - } - p { - margin: 0 20px; - } - - &.has-action-button { - padding-bottom: 80px; - position: relative; - .button { - bottom: 20px; - right: 20px; - position: absolute; - } - } - } -} - -@media only screen and (min-width: 600px) { - ul.llms-importable-courses { - display: flex; - flex-wrap: wrap; - } -} - -@media only screen and (min-width: 600px) and (max-width: 767px) { - ul.llms-importable-courses { - li.llms-importable-course { - flex: 1 0 calc( 50% - 20px ); - max-width: calc( 50% - 20px ); - } - } -} - -@media only screen and (min-width: 768px) { - ul.llms-importable-courses { - li.llms-importable-course { - flex: 1 0 calc( 33% - 20px ); - max-width: calc( 33% - 20px ); - } - } -} diff --git a/assets/scss/admin-setup.scss b/assets/scss/admin-setup.scss deleted file mode 100644 index 9a688ae95d..0000000000 --- a/assets/scss/admin-setup.scss +++ /dev/null @@ -1,178 +0,0 @@ -@import "_includes/vars"; - -#wpadminbar, #adminmenumain, #wpfooter { - display: none; -} - -#llms-setup-wizard { - - background-color: #f1f1f1; - height: 100%; - left: 0; - overflow: scroll; - position: fixed; - top: 0; - width: 100%; -} - -.llms-setup-wrapper { - margin: 20px auto; - max-width: 640px; -} - -#llms-logo { - text-align: center; - - img { - max-width: 200px; - } -} - -.llms-setup-content { - background-color: #fff; - box-shadow: 0 1px 3px rgba( 0, 0, 0, .13 ); - padding: 15px 30px; - - h1, h2, h3, h4, h5, h6 { - color: #444; - } - - a:not( .llms-button-primary ):not( .llms-button-secondary ) { - color: $color-brand-blue; - } - - p, li { - color: #666; - font-size: 16px; - } - - p.error { - color: $color-red; - text-align: center; - } - - label { - font-weight: 500; - } - - .llms-setup-actions { - margin-top: 40px; - text-align: right; - } - - .llms-exit-setup { - color: inherit !important; - margin-right: 10px; - } - - table { - border-bottom: 1px solid #f1f1f1; - border-collapse: collapse; - width: 100%; - } - - - td { - border-top: 1px solid #f1f1f1; - &:first-child { - padding-right: 10px; - width: 33%; - a { - font-size: 16px; - font-weight: 500; - } - } - } - - ul.llms-importable-courses { - display: block; - li.llms-importable-course { - border-bottom: 1px solid #f1f1f1; - display: block; - max-width: 100%; - padding-bottom: 15px; - - img { - float: left; - margin-right: 15px; - width: 20%; - } - - .llms-switch { - float: right; - - input.llms-toggle-round:checked + label { - border-color: $color-brand-blue; - background-color: $color-brand-blue; - } - - } - } - } - - - .llms-importing-msgs { - a { color: $color-brand-blue; } - .llms-importing-msg { - display: none; - font-size: 14px; - font-style: italic; - text-align: right; - } - } - -} - -.llms-setup-progress { - display: flex; - margin: 20px 0; - - li { - border-bottom: 4px solid $color-brand-blue; - display: inline-block; - font-size: 14px; - padding-bottom: 10px; - position: relative; - text-align: center; - flex: 1; - - a { - color: $color-brand-blue; - text-decoration: none; - } - - &:after { - background: $color-brand-blue; - bottom: 0; - content: ''; - border: 4px solid $color-brand-blue; - border-radius: 100%; - height: 4px; - position: absolute; - left: 50%; - margin-left: -6px; - margin-bottom: -8px; - width: 4px; - } - - &.current { - font-weight: 700; - &:after { - background: #fff; - } - } - - &.current ~ li { - border-bottom-color: #ccc; - &:after { - background: #ccc; - border-color: #ccc; - } - a { - color: #bbb; - } - } - - } -} - diff --git a/assets/scss/admin.scss b/assets/scss/admin.scss deleted file mode 100644 index fd85a22e9f..0000000000 --- a/assets/scss/admin.scss +++ /dev/null @@ -1,91 +0,0 @@ -// -// Main Admin CSS File -// - -@import "_includes/vars"; -@import "_includes/vars-brand-colors"; - -@import "_includes/extends"; -@import "_includes/buttons"; -@import "_includes/mixins"; - -@import "_includes/tooltip"; - -// wp menu item -@import "admin/_wp-menu"; - -// grid layout for breakpoints -@import "admin/partials/grid"; - -// forms -@import "admin/modules/forms"; - -// voucher -@import "admin/modules/voucher"; - -// widgets -@import "admin/modules/widgets"; - -// icons -@import "admin/modules/icons"; - -// icons -@import "admin/modules/mb-tabs"; - -// icons -@import "admin/modules/top-modal"; - -@import "admin/modules/merge-codes"; - -// Base (mobile) -@import "admin/breakpoints/base"; - -// Larger mobile devices -@media only screen and (min-width: 481px) { - @import "admin/breakpoints/481up"; -} - -// Tablets and smaller laptops -@media only screen and (min-width: 768px) { - @import "admin/breakpoints/768up"; -} - -// Desktops -@media only screen and (min-width: 1030px) { - @import "admin/breakpoints/1030up"; -} - -// Larger Monitors and TVs -@media only screen and (min-width: 1240px) { - @import "admin/breakpoints/1240up"; -} - -@import "admin/main"; - -@import "admin/llms-table"; -@import "admin/modules/llms-order-note"; - -// metabox related -@import "admin/metaboxes/llms-metabox"; -@import "admin/metaboxes/metabox-instructors"; -@import "admin/metaboxes/metabox-orders"; -@import "admin/metaboxes/metabox-product"; -@import "admin/metaboxes/metabox-students"; -@import "admin/metaboxes/metabox-field-repeater"; -@import "admin/metaboxes/builder-launcher"; - -@import "admin/post-tables/llms_orders"; -@import "admin/post-tables/post-tables"; - -@import "admin/tabs"; -@import "admin/fonts"; -@import "admin/reporting"; - -@import "admin/settings"; - -@import "admin/quiz-attempt-review"; - -@import "_includes/llms-form-field"; -@import "_includes/vendor/_font-awesome"; - -@import "_includes/spinner"; diff --git a/assets/scss/admin/_course-builder.scss b/assets/scss/admin/_course-builder.scss deleted file mode 100644 index 7a2ecbc217..0000000000 --- a/assets/scss/admin/_course-builder.scss +++ /dev/null @@ -1,1677 +0,0 @@ -body.admin_page_llms-course-builder { - background: #fff; - - #adminmenumain { display: none; } - #wpbody-content { padding-bottom: 0; } - #wpfooter { display: none; } - - #wpcontent, #wpfooter { - margin-left: 0; - } - - // &.folded { - // .llms-course-builder { - // left: 56px; - // } - // } - - .webui-popover { - .select2-container--default { - .select2-results__group { - font-size: 16px; - } - .select2-results__option .select2-results__option { - padding-left: 2em; - } - } - - } -} - - - - -// @media (max-width: 960px) { -// body.admin_page_llms-course-builder.auto-fold { -// .llms-course-builder { -// left: 56px; -// } -// } -// } - -.wrap.lifterlms.llms-builder { - margin: 0; - padding: 0; - position: relative; - - - &.editor-active { - .llms-builder-sidebar { - padding: 10px; - width: calc( 100% - 200px ); - z-index: 3; - } - @media only screen and ( min-width: 1200px ) { - .llms-builder-main { - width: 400px; - } - .llms-builder-sidebar { - width: calc( 100% - 640px ); - } - } - @media only screen and ( min-width: 1440px ) { - .llms-builder-main { - width: calc( 100% - 780px ); - } - .llms-builder-sidebar { - width: 720px; - } - } - @media only screen and ( min-width: 1680px ) { - .llms-builder-main { - width: calc( 100% - 1000px ); - } - .llms-builder-sidebar { - width: 940px; - } - } - } - - .llms-headline { - display: inline-block; - font-weight: 300; - margin: 0; - padding: 0; - transition: width 0.3s ease-in-out; - vertical-align: middle; - } - - .llms-builder-main { - padding: 20px 20px 20px 0; - position: relative; - width: calc( 100% - 340px ); - z-index: 2; - - .llms-action-icons { - display: inline-block; - left: -20px; - opacity: 0; - position: relative; - transition: left 0.2s ease, opacity 0.2s ease; - vertical-align: middle; - } - .llms-builder-header:hover > .llms-action-icons, - .llms-builder-header .llms-action-icons.static { - left: 0; - opacity: 1; - } - - /* - - - /$$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ - /$$_____/ /$$__ $$| $$ | $$ /$$__ $$ /$$_____/ /$$__ $$ - | $$ | $$ \ $$| $$ | $$| $$ \__/| $$$$$$ | $$$$$$$$ - | $$ | $$ | $$| $$ | $$| $$ \____ $$| $$_____/ - | $$$$$$$| $$$$$$/| $$$$$$/| $$ /$$$$$$$/| $$$$$$$ - \_______/ \______/ \______/ |__/ |_______/ \_______/ - */ - - .llms-course-header { - position: relative; - z-index: 1; - } - - - /* - /$$ /$$ - | $$ |__/ - /$$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$ - /$$_____/ /$$__ $$ /$$_____/|_ $$_/ | $$ /$$__ $$| $$__ $$ /$$_____/ - | $$$$$$ | $$$$$$$$| $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$ - \____ $$| $$_____/| $$ | $$ /$$| $$| $$ | $$| $$ | $$ \____ $$ - /$$$$$$$/| $$$$$$$| $$$$$$$ | $$$$/| $$| $$$$$$/| $$ | $$ /$$$$$$$/ - |_______/ \_______/ \_______/ \___/ |__/ \______/ |__/ |__/|_______/ - */ - ul.llms-sections { - box-shadow: 0 0 0 3px transparent; - min-height: 60px; - padding: 10px 0; - transition: box-shadow 0.6s ease, min-height 0.2s ease; - &.dragging { - box-shadow: 0 0 0 3px $color-brand-blue; - } - } - - li.llms-section { - background: #fff; - position: relative; - margin: 0; - padding: 20px 20px 20px 40px; - - &.expanded { - .llms-lessons { overflow: visible; } - } - &.selected { - .llms-drag-utility.drag-section { - border-color: $color-brand-blue; - } - > .llms-builder-header .llms-headline { - font-weight: 400; - color: $color-brand-blue; - } - } - - } - - // tree line - li.llms-section:before { - background: #ccc; - bottom: 0; - content: ''; - left: 19px; - position: absolute; - top: 0; - width: 2px; - } - - li.llms-section:first-child:before { - top: 30px; - } - - li.llms-section:last-child:before { - bottom: 55px; - } - - li.llms-section.expanded:last-child:before { - bottom: 86px; - } - - /* - /$$ - | $$ - | $$ /$$$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$$ - | $$ /$$__ $$ /$$_____//$$_____/ /$$__ $$| $$__ $$ /$$_____/ - | $$| $$$$$$$$| $$$$$$| $$$$$$ | $$ \ $$| $$ \ $$| $$$$$$ - | $$| $$_____/ \____ $$\____ $$| $$ | $$| $$ | $$ \____ $$ - | $$| $$$$$$$ /$$$$$$$//$$$$$$$/| $$$$$$/| $$ | $$ /$$$$$$$/ - |__/ \_______/|_______/|_______/ \______/ |__/ |__/|_______/ - */ - ul.llms-lessons { - box-shadow: 0 0 0 3px transparent; - height: 0; - margin: 10px 0 0; - overflow: hidden; - padding: 10px 0; - transition: box-shadow 0.6s ease, min-height 0.2s ease; - &.dragging { - box-shadow: 0 0 0 3px $color-brand-blue; - min-height: 60px; - } - &.expanded, // added via backbone view events - &.drag-expanded { // added only during dragover events and ignores model attrs - height: auto; - li.llms-lesson { - pointer-events: auto; - visibility: visible; - } - } - - } - - li.llms-lesson { - background: #fff; - margin: 0; - padding: 10px 20px 10px 30px; - position: relative; - pointer-events: none; - visibility: hidden; - - &.selected { - .llms-drag-utility.drag-lesson { - border-color: $color-brand-blue; - } - > .llms-builder-header .llms-headline { - font-weight: 400; - color: $color-brand-blue; - } - } - - } - - // line on left of each lesson - li.llms-lesson:before { - background: #ccc; - content: ''; - height: 2px; - left: -20px; - position: absolute; - top: 20px; - width: 30px; - } - - - /* - /$$ /$$ /$$ /$$ - | $$ | $$ | $$| $$ - /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ | $$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$$| $$ /$$$$$$ /$$$$$$$ - /$$__ $$ /$$__ $$|____ $$ /$$__ $$ | $$__ $$ |____ $$| $$__ $$ /$$__ $$| $$ /$$__ $$ /$$_____/ - | $$ | $$| $$ \__/ /$$$$$$$| $$ \ $$ | $$ \ $$ /$$$$$$$| $$ \ $$| $$ | $$| $$| $$$$$$$$| $$$$$$ - | $$ | $$| $$ /$$__ $$| $$ | $$ | $$ | $$ /$$__ $$| $$ | $$| $$ | $$| $$| $$_____/ \____ $$ - | $$$$$$$| $$ | $$$$$$$| $$$$$$$ | $$ | $$| $$$$$$$| $$ | $$| $$$$$$$| $$| $$$$$$$ /$$$$$$$/ - \_______/|__/ \_______/ \____ $$ |__/ |__/ \_______/|__/ |__/ \_______/|__/ \_______/|_______/ - /$$ \ $$ - | $$$$$$/ - \______/ - */ - li.llms-section .llms-drag-utility { - background: #fff; - border: 2px solid #ccc; - border-radius: 50%; - height: 10px; - left: 13px; - position: absolute; - top: 24px; - width: 10px; - } - - li.llms-lesson .llms-drag-utility { - height: 6px; - left: 5px; - top: 16px !important; - width: 6px; - } - - .llms-section:hover > .llms-drag-utility, - .llms-lesson:hover > .llms-drag-utility { - border-color: #fff; - cursor: move; - &:hover:after { - color: $color-brand-blue; - } - &:after { - background: #fff; - content: '\00b7\00b7\A\00b7\00b7\A\00b7\00b7'; - color: #ccc; - display: block; - font-size: 36px; - height: 29px; - letter-spacing: -1px; - line-height: 8px; - left: -7px; - position: absolute; - text-align: center; - top: -12px; - width: 23px; - } - } - - /* - /$$ /$$ /$$ - | $$ | $$ | $$ - /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ | $$$$$$$ | $$ /$$$$$$ - /$$_____/ /$$__ $$ /$$__ $$|_ $$_/ |____ $$| $$__ $$| $$ /$$__ $$ - | $$$$$$ | $$ \ $$| $$ \__/ | $$ /$$$$$$$| $$ \ $$| $$| $$$$$$$$ - \____ $$| $$ | $$| $$ | $$ /$$ /$$__ $$| $$ | $$| $$| $$_____/ - /$$$$$$$/| $$$$$$/| $$ | $$$$/| $$$$$$$| $$$$$$$/| $$| $$$$$$$ - |_______/ \______/ |__/ \___/ \_______/|_______/ |__/ \_______/ - */ - li.llms-section, - li.llms-lesson { - &.ui-sortable-helper, - &.ui-draggable-dragging { - border: 1px solid #ccc; - background: #fff; - transform: rotate( 2deg ); - visibility: visible !important; - z-index: 999; - - // hide tree line on the helper - &:before { display: none; } - - // prevent action icon hover display - .llms-action-icons, - .llms-builder-header:hover > .llms-action-icons { - display: none; - } - } - - &.llms-sortable-placeholder { - border: 3px dashed $color-brand-blue; - background: rgba( $color-brand-blue, 0.3 ); - margin: 0 10px; - padding: 5px; - &:before { display: none; } - } - } - - ul.llms-sections > li.llms-lesson.ui-draggable-dragging .llms-drag-utility { - position: relative; - &:after { - left: -35px; - top: -28px; - } - } - - } - - /* - /$$ /$$ /$$ /$$ /$$ - | $$|__/ | $$ | $$ | $$ - /$$$$$$ /$$$$$$$ /$$ /$$$$$$ /$$$$$$ | $$$$$$$ | $$ /$$$$$$ - /$$__ $$ /$$__ $$| $$|_ $$_/ |____ $$| $$__ $$| $$ /$$__ $$ - | $$$$$$$$| $$ | $$| $$ | $$ /$$$$$$$| $$ \ $$| $$| $$$$$$$$ - | $$_____/| $$ | $$| $$ | $$ /$$ /$$__ $$| $$ | $$| $$| $$_____/ - | $$$$$$$| $$$$$$$| $$ | $$$$/| $$$$$$$| $$$$$$$/| $$| $$$$$$$ - \_______/ \_______/|__/ \___/ \_______/|_______/ |__/ \_______/ - */ - - .llms-input-wrapper { - position: relative; - } - - .llms-input-formatting.ql-container { - font-size: inherit; - font-family: inherit; - .ql-editor.ql-blank::before { - color: #a0a0a0; - left: 8px; - right: 8px; - } - .ql-editor { - p { - font-size: inherit; - line-height: 1; - } - } - .ql-tooltip { - z-index: 1; - } - } - - .llms-input, - .llms-input-formatting .ql-editor { - border: none; - border-bottom: 2px dotted transparent; - box-shadow: none; - cursor: text; - display: inline-block; - font-size: inherit; - font-weight: 500; - height: auto; - line-height: 1; - margin: 0 8px; - min-width: 60px; - padding: 0; - transition: border 0.2s ease, box-shadow 0.2s ease; - &:empty:before { - color: #a0a0a0; - content: attr( data-placeholder ); - } - &:hover { - border-bottom-color: $color-brand-blue; - } - &[disabled] { - cursor: not-allowed; - &:hover { - border-bottom-color: transparent; - } - } - &:focus { - background: #fff; - box-shadow: 0 0 0 4px #fff, 0 0 0 6px $color-brand-blue; - border-bottom: none; - outline: none; - } - b, strong { - font-weight: 700; - } - &.standard { - border: 1px solid #e6e6e6; - margin: 2px; - padding: 5px 3px; - &:hover { - border-color: #d6d6d6; - } - &:focus { - box-shadow: 0 0 0 2px $color-brand-blue; - } - } - &.permalink { - display: none; - } - } - - .llms-input-formatting .ql-editor { - padding: 0 1px; - } - - .llms-label { - font-weight: 500; - .fa { - color: #aaa; - padding-left: 6px; - } - } - - // .llms-editable-image, - // .llms-editable-video, - // .llms-editable-editor { - // } - - .llms-editable-editor { - .llms-label { - float: left; - margin-right: 10px; - position: relative; - top: 10px; - } - textarea { - border: none; - padding: 10px; - display: block; - width: 100%; - } - } - - .llms-editable-image { - button.llms-add-image { - width: 130px; - } - .llms-image { - display: inline-block; - position: relative; - &:hover .llms-action-icon { - opacity: 1; - } - .llms-action-icon { - color: #fff; - font-size: 24px; - opacity: 0; - padding: 0; - position: absolute; - transition: opacity 0.2s ease; - right: 3px; - top: 1px; - z-index: 1; - } - img { - display: block; - height: 100px; - max-width: 100%; - width: auto; - } - } - } - - .llms-settings-field, - .llms-editable-toggle-group { - background: #f4f4f4; - padding: 10px; - position: relative; - margin: 0 1px; - - &.has-label-after { - align-items: center; - display: flex; - flex-wrap: wrap; - - .llms-label { - min-width: 100%; - } - .llms-editable-input { - flex: 2; - } - .llms-label--after { - color: #888; - min-width: auto; - font-size: 85%; - padding-left: 10px; - } - } - - .llms-switch { - display: block; - width: 100%; - @include clearfix; - - .llms-label { - width: calc( 100% - 34px ); - } - } - - .llms-editable-image, - .llms-editable-video, - .llms-editable-editor { - margin-top: 2px; - } - - .llms-input.standard { - display: block; - width: 100%; - &.two-digits, - &.three-digits, - &.four-digits { - display: inline-block; - } - } - - } - - .llms-editable-number { - .llms-input { - color: #888; - min-width: 30px; - text-align: right; - &.two-digits { - width: 30px; - } - &.three-digits { - width: 40px; - } - &.four-digits { - width: 60px; - } - } - small { - color: #888; - text-transform: uppercase; - } - } - - .llms-model-settings { - .llms-settings-group-header { - .fa-caret-square-o-up { display: block; } - .fa-caret-square-o-down { display: none; } - } - &.hidden { - .llms-settings-group-header { - .fa-caret-square-o-up { display: none; } - .fa-caret-square-o-down { display: block; } - } - .llms-settings-group-body { display: none; } - } - } - - .llms-settings-group-header { - @include clearfix(); - .llms-settings-group-title { - display: inline-block; - font-size: 16px; - font-weight: 300; - margin: 0 5px; - padding: 0; - } - .llms-settings-group-toggle { - float: right; - font-size: 18px; - padding: 2px; - } - } - - .llms-settings-group-body { - margin-top: 5px; - } - - .llms-settings-row { - display: flex; - flex-wrap: wrap; - margin: 2px 0; - - .llms-settings-field, - .llms-editable-toggle-group { - flex: 1; - &:first-child { - margin-left: 0; - } - &:last-child { - margin-right: 0; - } - } - - .llms-breaker { - margin: 2px 0; - width: 100%; - } - } - - .llms-editable-select { - margin: 2px 0; - .select2-container--default.select2-container--focus .select2-selection--multiple { - border-color: #aaa; - } - } - - .llms-editable-radio { - label { - display: block; - } - &.has-images { - input { display: none; } - label { - display: inline-block; - margin: 0 3px; - } - label > span { - transition: background 0.2s ease; - display: inline-block; - padding: 3px; - } - img { display: block; } - input:checked + span { - background: $color-brand-blue; - } - } - } - - /* - /$$ - |__/ - /$$ /$$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$$ - | $$ /$$_____/ /$$__ $$| $$__ $$ /$$_____/ - | $$| $$ | $$ \ $$| $$ \ $$| $$$$$$ - | $$| $$ | $$ | $$| $$ | $$ \____ $$ - | $$| $$$$$$$| $$$$$$/| $$ | $$ /$$$$$$$/ - |__/ \_______/ \______/ |__/ |__/|_______/ - */ - .llms-action-icon { - color: #aaa; - display: inline-block; - font-size: 16px; - padding: 0 5px; - text-decoration: none; - &:hover { - color: $color-brand-blue; - &.danger { color: $color-danger; } - } - &.circle { - border: 2px solid #aaa; - border-radius: 50%; - font-size: 9px; - height: 8px; - line-height: 1; - padding: 5px; - text-align: center; - width: 8px; - &:hover { - border-color: $color-brand-blue; - &.danger { - border-color: $color-danger; - } - } - } - } - - ul.llms-info-list { - @include clearfix(); - margin: 0; - padding: 0; - li.llms-info-item { - color: #aaa; - font-size: 16px; - margin: 0; - padding: 10px; - float: left; - &.active, - &.active .llms-action-icon { - color: $color-brand-blue; - } - .llms-action-icon { - margin: -10px; - padding: 10px; - } - } - - } - - /* - /$$ /$$ /$$ - |__/ | $$ | $$ - /$$$$$$$ /$$ /$$$$$$$ /$$$$$$ | $$$$$$$ /$$$$$$ /$$$$$$ - /$$_____/| $$ /$$__ $$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$ - | $$$$$$ | $$| $$ | $$| $$$$$$$$| $$ \ $$ /$$$$$$$| $$ \__/ - \____ $$| $$| $$ | $$| $$_____/| $$ | $$ /$$__ $$| $$ - /$$$$$$$/| $$| $$$$$$$| $$$$$$$| $$$$$$$/| $$$$$$$| $$ - |_______/ |__/ \_______/ \_______/|_______/ \_______/|__/ - */ - .llms-builder-sidebar { - background: #e6e6e6; - bottom: 0; - overflow: hidden; - padding: 20px; - position: fixed; - transition: width 0.3s ease-in-out; - top: 32px; - right: 0; - width: 280px; - z-index: 1; - - .llms-utilities { - - ul, li { - margin: 0; - padding: 0; - } - - ul { - display: flex; - li { - flex: 1; - margin-right: 5px; - &:last-child { - margin-right: 0; - } - } - } - - - a.llms-utility { - background: #efefef; - border-radius: 4px; - display: block; - overflow: hidden; - padding: 4px; - position: relative; - text-align: center; - - &:hover { - background: #fefefe; - } - - .fa { - background: #848484; - position: absolute; - left: 0; - top: 0; - padding: 7px; - color: #fff; - } - } - - } - - .llms-sidebar-headline { - margin: 0 0 10px; - font-size: 22px; - font-weight: 300; - letter-spacing: 2px; - text-transform: uppercase; - } - - .llms-elements-list { - margin-bottom: 40px; - li { - margin-bottom: 10px; - } - } - - .llms-utility { - color: #444; - text-decoration: none; - } - - .llms-element-button { - - background: $color-brand-blue; - border-radius: 4px; - border: none; - color: #fff; - cursor: pointer; - display: block; - margin: 0; - overflow: hidden; - padding: 17px 20px; - position: relative; - transition: background 0.2s ease, color 0.2s ease; - text-align: center; - width: 100%; - - &:hover { - background: $color-brand-blue-dark; - } - - &.secondary { - background: #efefef; - color: #444; - &:hover { - background: #fefefe; - } - .fa { - background: #848484; - } - } - - .fa { - background: $color-brand-dark-blue; - border-radius: 4px 0 0 4px; - color: #fff; - display: block; - font-size: 20px; - padding: 15px 20px; - position: absolute; - top: 0; - left: 0; - } - - &[disabled="disabled"] { - opacity: 0.4; - } - - &.small { - - padding: 8px 10px 8px 46px; - .fa { - font-size: 15px; - padding: 9px 10px; - width: 20px; - } - - } - - &.right { - - &.small { - padding-left: 10px; - padding-right: 46px; - } - - .fa { - border-radius: 0 4px 4px 0; - left: auto; - right: 0; - } - - } - - } - - - - .llms-editor { - height: 100%; - min-height: 100%; - position: relative; - } - - // .llms-builder-close-editor { - // background: $color-brand-blue; - // border: none; - // border-radius: 50%; - // color: #fff; - // cursor: pointer; - // display: inline-block; - // font-size: 18px; - // height: 30px; - // margin: 0; - // position: absolute; - // right: 0; - // text-align: center; - // top: 3px; - // width: 30px; - // z-index: 3; - // } - - .llms-editor-nav { - background: #b0b0b0; - font-size: 0; - margin: -10px -10px 10px -10px; - position: relative; - z-index: 2; - - .llms-editor-menu { - list-style-type: none; - margin: 0; - padding: 0; - position: relative; - - .llms-editor-menu-item { - display: inline-block; - margin: 0; - padding: 0; - - &.right { - float: right; - } - - > .llms-editor-menu { - display: none; - &:before { - border: 8px solid transparent; - border-left-color: #cacaca; - content: ''; - position: absolute; - top: 11px; - left: 0; - } - - .llms-editor-menu-item:hover > a, - .llms-editor-menu-item.active > a { - background: #dfdfdf; - } - - } - - &:hover > a, - &.active > a { - background: #cacaca; - } - - &.active > a { - box-shadow: inset 0 -3px 0 $color-brand-blue; - &:focus { - box-shadow: inset 0 -3px 0 $color-brand-blue; - } - } - - &.active > .llms-editor-menu { - display: inline-block; - } - - a { - color: #444; - display: inline-block; - padding: 13px 20px; - text-decoration: none; - transition: background 0.2s ease; - font-size: 14px; - &:focus { - box-shadow: inset 0 0 0 2px $color-brand-blue; - } - } - - } - } - } - - .llms-editor-tab { - display: none; - height: calc( 100% - 90px ); - overflow: scroll; - position: relative; - z-index: 1; - &.active { - display: block; - - &.tab--quiz { - - display: flex; - flex-direction: column; - - .llms-quiz-questions { - flex: 1; - overflow: scroll; - - // groups - .llms-quiz-questions { - overflow: visible; - } - } - - } - } - - } - - - // .llms-builder-editor { - - // opacity: 0; - // margin: 10px 0; - // transition: opacity 0.2s linear; - - // &.ready { - // opacity: 1; - // } - - // textarea { - // border: none; - // display: block; - // width: 100%; - // } - // } - - .llms-builder-save { - - bottom: 10px; - left: 10px; - position: absolute; - right: 10px; - z-index: 1; - - .llms-builder-error { - background: $color-danger; - border-radius: 4px; - color: #fff; - display: inline-block; - font-style: italic; - padding: 5px 15px 7px 25px; - margin: 0 0 10px; - - li { - margin: 0; - padding: 0; - } - - } - - .llms-save { - width: 75%; - } - .llms-exit { - width: 23%; - } - - button { - position: relative; - i { - position: absolute; - left: 10px; - top: 10px; - - .llms-spinner { - border-color: #fff; - } - } - } - button[data-status] .llms-status-indicator { display: none; } - button[data-status="saved"] .status--saved { display: block; } - button[data-status="unsaved"] .status--unsaved { display: block; } - button[data-status="saving"] .status--saving { display: block; } - button[data-status="error"] .status--error { display: block; } - - } - - } - - - /* - /$$ - | $$ - /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$| $$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ - /$$_____/ /$$__ $$ |____ $$ /$$__ $$ /$$_____/| $$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$| $$ /$$//$$__ $$ /$$__ $$ - | $$$$$$ | $$$$$$$$ /$$$$$$$| $$ \__/| $$ | $$ \ $$ | $$ \ $$| $$ \ $$| $$ \ $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \__/ - \____ $$| $$_____/ /$$__ $$| $$ | $$ | $$ | $$ | $$ | $$| $$ | $$| $$ | $$| $$ | $$ \ $$$/ | $$_____/| $$ - /$$$$$$$/| $$$$$$$| $$$$$$$| $$ | $$$$$$$| $$ | $$ | $$$$$$$/| $$$$$$/| $$$$$$$/| $$$$$$/ \ $/ | $$$$$$$| $$ - |_______/ \_______/ \_______/|__/ \_______/|__/ |__/ | $$____/ \______/ | $$____/ \______/ \_/ \_______/|__/ - | $$ | $$ - | $$ | $$ - |__/ |__/ - */ - - .select2-container { - z-index: 99999999; - } - - .select2-results__option { - padding: 0; - } - - .select2-container--default .select2-results__option--highlighted[aria-selected] { - background: $color-brand-blue; - .llms-existing-action { - color: #fff; - } - } - - .llms-existing-lesson-result { - - align-items: center; - display: flex; - padding: 5px 5px 5px 0; - - .llms-existing-info { - flex: 6; - - h4, h5 { - margin: 0; - } - - h4 { - font-weight: 400; - } - - h5 { - font-weight: 300; - } - } - - .llms-existing-action { - color: $color-brand-blue; - flex: 1; - text-align: center; - - .fa { - display: block; - font-size: 30px; - } - - small { - text-transform: uppercase; - } - - } - - - } - - - /* - /$$ - |__/ - /$$$$$$ /$$ /$$ /$$ /$$$$$$$$ - /$$__ $$| $$ | $$| $$|____ /$$/ - | $$ \ $$| $$ | $$| $$ /$$$$/ - | $$ | $$| $$ | $$| $$ /$$__/ - | $$$$$$$| $$$$$$/| $$ /$$$$$$$$ - \____ $$ \______/ |__/|________/ - | $$ - | $$ - |__/ - */ - - .llms-quiz-empty { - margin: 100px auto; - text-align: center; - - p { font-size: 18px; } - button.llms-element-button { - max-width: 320px; - margin: 0 auto; - } - - } - - - .llms-model-header, - .llms-model-settings { - background: #fff; - padding: 10px; - @include clearfix(); - } - - .llms-editor-tab.tab--quiz { - .llms-model-header { - .llms-model-title { - width: calc( 100% - 310px ); - } - .llms-quiz-points { - float: left; - margin-right: 10px; - width: 100px; - } - } - } - - .llms-model-header { - border-bottom: 5px solid #e6e6e6; - - .llms-model-title { - float: left; - margin-right: 10px; - width: calc( 100% - 200px ); - .llms-input { - width: calc( 100% - 65px ); - } - } - .llms-model-status.llms-switch { - float: left; - margin-right: 10px; - position: relative; - text-align: right; - top: -2px; - width: 100px; - } - .llms-action-icons { - float: left; - position: relative; - text-align: right; - width: 80px; - z-index: 1; - .fa { - max-width: 15px; - } - } - - .llms-model-settings { - padding: 0; - } - - } - - .llms-model-header + .llms-model-settings.active { - margin-top: -10px; - } - - .llms-model-settings { - clear: both; - display: none; - - &.active { - display: block; - margin-top: 10px; - } - } - - .llms-quiz-footer { - display: flex; - button.llms-element-button { - flex: 1; - margin: 0 5px; - &:first-child { margin-left: 0; } - &:last-child { margin-right: 0; } - &.llms-show-question-bank { - flex: 2; - } - } - } - - /* - /$$ /$$ /$$ /$$ - | $$ |__/ | $$ | $$ - /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$ /$$$$$$$ | $$$$$$$ /$$$$$$ /$$$$$$$ | $$ /$$ - /$$__ $$| $$ | $$ /$$__ $$ /$$_____/|_ $$_/ | $$ /$$__ $$| $$__ $$ | $$__ $$ |____ $$| $$__ $$| $$ /$$/ - | $$ \ $$| $$ | $$| $$$$$$$$| $$$$$$ | $$ | $$| $$ \ $$| $$ \ $$ | $$ \ $$ /$$$$$$$| $$ \ $$| $$$$$$/ - | $$ | $$| $$ | $$| $$_____/ \____ $$ | $$ /$$| $$| $$ | $$| $$ | $$ | $$ | $$ /$$__ $$| $$ | $$| $$_ $$ - | $$$$$$$| $$$$$$/| $$$$$$$ /$$$$$$$/ | $$$$/| $$| $$$$$$/| $$ | $$ | $$$$$$$/| $$$$$$$| $$ | $$| $$ \ $$ - \____ $$ \______/ \_______/|_______/ \___/ |__/ \______/ |__/ |__/ |_______/ \_______/|__/ |__/|__/ \__/ - | $$ - | $$ - |__/ - */ - - .llms-quiz-tools { - display: none; - width: 100%; - position: relative; - - // .llms-quiz-tools-search { - // padding: 0 10px; - // margin-bottom: 15px; - - // .fa { - // color: #888; - // font-size: 16px; - // } - - // input[type="search"] { - // background: inherit; - // border: none; - // border-bottom: 1px solid #bbb; - // box-shadow: none; - // font-size: 16px; - // margin: 8px 0 0; - // padding: 2px 5px; - // width: calc( 100% - 200px ); - - // &:focus { - // border-bottom-color: $color-brand-blue; - // } - // } - - // } - - } - - ul.llms-question-bank { - - list-style-type: none; - margin: 0; - padding: 0; - @include clearfix; - - li.llms-question-bank-header { - clear: both; - padding-top: 20px; - &:first-child { - padding-top: 0; - } - h4 { - font-size: 20px; - margin: 10px 5px; - } - } - - li.llms-question-type { - box-sizing: border-box; - float: left; - margin: 0; - padding: 3px; - width: 33.3333%; - transition: opacity 0.3s ease-in-out; - - &.filtered { - opacity: 0.3; - } - - .llms-type-unavailable { - display: block; - position: relative; - text-decoration: none; - .llms-element-button { - opacity: 0.5; - pointer-events: none; - } - } - - } - - } - - /* - /$$ /$$ /$$ - |__/ | $$ |__/ - /$$$$$$ /$$ /$$ /$$ /$$$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$ - /$$__ $$| $$ | $$| $$|____ /$$/ /$$__ $$| $$ | $$ /$$__ $$ /$$_____/|_ $$_/ | $$ /$$__ $$| $$__ $$ /$$_____/ - | $$ \ $$| $$ | $$| $$ /$$$$/ | $$ \ $$| $$ | $$| $$$$$$$$| $$$$$$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$ - | $$ | $$| $$ | $$| $$ /$$__/ | $$ | $$| $$ | $$| $$_____/ \____ $$ | $$ /$$| $$| $$ | $$| $$ | $$ \____ $$ - | $$$$$$$| $$$$$$/| $$ /$$$$$$$$ | $$$$$$$| $$$$$$/| $$$$$$$ /$$$$$$$/ | $$$$/| $$| $$$$$$/| $$ | $$ /$$$$$$$/ - \____ $$ \______/ |__/|________/ \____ $$ \______/ \_______/|_______/ \___/ |__/ \______/ |__/ |__/|_______/ - | $$ | $$ - | $$ | $$ - |__/ |__/ - */ - ul.llms-quiz-questions { - - margin: 10px 3px; - padding: 5px; - transition: box-shadow 0.6s ease; - - &.dragging { - box-shadow: 0 0 0 3px $color-brand-blue; - } - - &:empty:before { - background: #fff; - content: attr(data-empty-msg); - display: block; - font-size: 18px; - margin: 0 auto; - padding: 100px 0; - text-align: center; - } - - li.llms-question { - - background: #fff; - margin: 0 0 3px; - padding: 15px 12px 10px; - - &:hover { - > .llms-builder-header .llms-action-icons { - opacity: 1; - pointer-events: auto; - } - } - - // groups - ul.llms-quiz-questions { - margin-left: 12px; - .llms-question { - border-bottom: 2px solid #e6e6e6; - } - &:empty:before { - content: attr(data-empty-msg); - display: block; - font-size: 18px; - text-align: center; - margin: 20px auto; - } - li.llms-question.llms-sortable-placeholder.qtype--group { - display: none !important; - } - } - - .llms-builder-header { - @include clearfix; - > * { - float: left; - } - } - - .llms-question-body { - display: none; - &.active { - display: block; - } - } - - .llms-data-stamp { - background: $color-brand-blue; - border-radius: 4px; - color: #fff; - cursor: move; - font-size: 90%; - margin-top: -5px; - padding: 4px 10px 6px; - - small, .fa { - line-height: 1.2; - vertical-align: middle; - } - - .fa { - margin-right: 4px; - } - - } - - .llms-headline { - width: calc( 100% - 110px - 90px - 55px ); - .ql-editor { - width: calc( 100% - 16px ); - } - } - - .llms-action-icons { - width: 110px; - opacity: 0; - pointer-events: none; - } - - .llms-question-points { - width: 90px; - } - - .llms-question-features { - margin: 10px 0 0; - &:last-child { - margin: 0; - } - .llms-switch { - margin-right: 15px; - } - } - - .llms-editable-video { - position: relative; - z-index: 1; - } - - } - - .llms-question-choices-wrapper { - background: #f4f4f4; - margin: 2px 1px; - padding: 10px; - } - - .llms-question-choices-list-header { - @include clearfix; - margin-bottom: 10px; - - .llms-switch { - float: right; - text-align: right; - width: 260px; - } - } - - ul.llms-question-choices { - border: 3px solid #f4f4f4; - margin: -3px; - padding: 0; - transition: box-shadow 0.6s ease; - - &.dragging { - box-shadow: 0 0 0 3px $color-brand-blue; - } - - &.multi-choices li.llms-question-choice .llms-choice-id span { - border-radius: 4px; - } - - } - - li.llms-question-choice { - margin: 0 0 5px; - padding: 0; - &:last-child { margin-bottom: 0; } - - .llms-choice-id { - - input[type="checkbox"] { - display: none; - } - - input[type="checkbox"]:checked + .llms-marker { - background: $color-green; - } - - .llms-marker { - border-radius: 50%; - background: #d0d0d0; - box-shadow: inset 0 0 1px #848484; - color: #444; - display: inline-block; - font-size: 16px; - height: 20px; - line-height: 20px; - padding: 5px; - position: relative; - text-align: center; - transition: background 0.1s ease; - width: 20px; - - .fa { - left: 7px; - opacity: 0; - position: absolute; - top: 7px; - } - &.selectable:hover { - b { opacity: 0 } - .fa { opacity: 1; } - } - - } - - } - - .llms-input-wrapper, - .llms-editable-image { - display: inline-block; - // action icons width, label width, ul margins - width: calc( 100% - 55px - 35px - 5px ); - } - - .llms-input { - width: calc( 100% - 16px ); - } - - .llms-editable-image .llms-image { - vertical-align: middle; - img { - height: 50px; - } - } - - .llms-action-icons { - display: inline-block; - opacity: 1; - pointer-events: auto; - text-align: right; - width: 55px; - } - - } - - li.llms-question-choice.llms-sortable-placeholder { - border: 3px dashed $color-brand-blue !important; - background: rgba( $color-brand-blue, 0.3 ); - } - - li.llms-question-choice.ui-sortable-helper { - border: 1px solid #ccc; - background: #fff; - padding: 10px; - transform: rotate( 2deg ); - z-index: 999; - } - - li.llms-question.ui-sortable-helper, - li.llms-question.ui-draggable-dragging { - border: 1px solid #ccc; - background: #fff; - transform: rotate( 2deg ); - z-index: 999; - } - - li.llms-question.llms-sortable-placeholder { - border: 3px dashed $color-brand-blue !important; - background: rgba( $color-brand-blue, 0.3 ); - } - - } - - - .llms-switch { - display: inline-block; - float: none; - width: auto; - - input[type="checkbox"] { - display: none; - } - - input[type="checkbox"]:checked + .llms-switch-slider { - background: $color-green; - } - - input[type="checkbox"]:checked + .llms-switch-slider:after { - transform: translateX( 14px ); - } - - .llms-label { - display: inline-block; - vertical-align: top; - } - - .llms-switch-slider { - background: #e0e0e0; - border-radius: 8px; - display: inline-block; - height: 16px; - margin-top: 2px; - position: relative; - transition: background 0.2s ease; - vertical-align: top; - width: 30px; - - &:after { - background: #fff; - border-radius: 8px; - content: ''; - display: block; - height: 12px; - left: 2px; - position: relative; - transition: transform 0.2s ease; - top: 2px; - width: 12px; - } - - } - - } - -} diff --git a/assets/scss/admin/_fonts.scss b/assets/scss/admin/_fonts.scss deleted file mode 100644 index d2b834e87e..0000000000 --- a/assets/scss/admin/_fonts.scss +++ /dev/null @@ -1,8 +0,0 @@ -#llms-options-page-contents { - h2 { - color: #999; - font-weight: 500; - letter-spacing: 2px; - border-bottom: 1px solid #999; - } -} diff --git a/assets/scss/admin/_llms-table.scss b/assets/scss/admin/_llms-table.scss deleted file mode 100644 index d1afc175fc..0000000000 --- a/assets/scss/admin/_llms-table.scss +++ /dev/null @@ -1,213 +0,0 @@ -.llms-table-wrap { - position: relative; -} - -.llms-table-header { - padding: 0 10px; - margin-bottom: 10px; - - @include clearfix(); - - h2 { - padding: 0; - display: inline-block; - line-height: 1; - margin: 0; - vertical-align: middle; - } - - .llms-table-search, - .llms-table-filters { - float: right; - padding-left: 10px; - } - - .llms-table-search input { - margin: 0; - padding: 5px; - } - -} - -.llms-table { - - border: 1px solid #cecece; - border-collapse: collapse; - width: 100%; - - a { - color: $color-brand-blue; - &:hover { - color: $color-brand-blue-dark; - } - } - - td, th { - border-bottom: 1px solid #cecece; - font-size: 95%; - padding: 4px; - text-align: center; - - &.expandable.closed { - display: none; - } - - .llms-button-primary, - .llms-button-secondary, - .llms-button-action, - .llms-button-danger { - display: inline-block; - } - - } - - tr.llms-quiz-pending { - td { - font-weight: 700; - } - } - - thead th, - tfoot th { - background-color: #eaeaea; - font-weight: 500; - - a.llms-sortable { - // display: block; - padding-right: 16px; - position: relative; - text-decoration: none; - width: 100%; - &.active { - // show the current sorted when a sort is active - &[data-order="DESC"] .asc { opacity: 1; } - &[data-order="ASC"] .desc { opacity: 1; } - } - // show the opposite on hover - &:hover { - &[data-order="DESC"] { - .asc { opacity: 0; } - .desc { opacity: 1; } - } - &[data-order="ASC"] { - .asc { opacity: 1; } - .desc { opacity: 0; } - } - } - .dashicons { - color: #444; - font-size: 16px; - height: 16px; - opacity: 0; - position: absolute; - width: 16px; - } - } - } - - tfoot th { - border-bottom: none; - - .llms-table-export { - float: left; - .llms-table-progress { - background: #fafafa; - display: none; - margin-left: 8px; - vertical-align: middle; - width: 100px; - } - } - - .llms-table-pagination { - float: right; - } - - } - - &.zebra tbody tr:nth-child( even ) { - th, td { background-color: #fafafa; } - } - - &.zebra tbody tr:nth-child( odd ) { - th, td { background-color: #fff; } - } - - &.text-left { - td, th { - text-align: left; - } - } - - &.size-large { - td, th { - font-size: 105%; - padding: 8px; - } - } - - .llms-action-icon { - color: #777; - text-decoration: none; - - .tooltip { - cursor: pointer; - } - - &:hover { - color: $color-blue; - } - - &.danger:hover { - color: $color-danger; - } - } - - .llms-table-page-count { - font-size: 11px; - font-weight: 300; - padding: 0 5px; - } - -} - -// progress bars within the tables -.llms-table-progress { - background: #eee; - height: 16px; - position: relative; - .llms-table-progress-text { - font-size: 11px; - line-height: 16px; - position: absolute; - right: 4px; - top: 0; - } - .llms-table-progress-inner { - background: $color-brand-blue; - height: 100%; - transition: width 0.2s ease; - } -} - - -.llms-table.llms-gateway-table, -.llms-table.llms-integrations-table { - .status { - .fa { - color: $color-brand-blue; - font-size: 22px; - } - } - .sort { - cursor: move; - text-align: center; - width: 10px; - } -} - -.llms-gb-table-notifications { - th, td { - text-align: left; - } -} diff --git a/assets/scss/admin/_main.scss b/assets/scss/admin/_main.scss deleted file mode 100644 index f420a91301..0000000000 --- a/assets/scss/admin/_main.scss +++ /dev/null @@ -1,94 +0,0 @@ -#post_course_difficulty { - min-width: 200px; -} -#_video-embed, #_audio-embed { - width: 100%; -} - -.clear { - clear: both; - width: 100%; -} - -.llms_certificate_default_image, .llms_certificate_image { - width: 300px; -} - -.llms_achievement_default_image, .llms_achievement_image { - width: 120px; -} - -div[id^="lifterlms-"] .inside { - overflow: visible; -} - -.llms-admin-notice { - position: relative; - .notice-dismiss { - text-decoration: none; - } -} - -.llms-button-action, -.llms-button-danger, -.llms-button-primary, -.llms-button-secondary { - &.small .dashicons { - font-size: 13px; - height: 13px; - width: 13px; - } -} - -a.llms-view-as { - line-height: 2; - margin-right: 8px; -} - -.llms-image-field-preview { - max-height: 80px; - vertical-align: middle; - width: auto; -} - -.llms-image-field-remove { - &.hidden { display: none; } -} - -.llms-log-viewer { - background: #fff; - border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba(0,0,0,.04); - margin: 20px 0; - padding: 25px; - - pre { - font-family: monospace; - margin: 0; - padding: 0; - white-space: pre-wrap; - } -} - -.llms-status--tools { - .llms-table { - background: #fff; - border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba(0,0,0,.04); - td, th { - padding: 10px; - vertical-align: top; - } - th { - width: 28%; - } - p { - margin: 0 0 10px; - } - } -} - -.llms-error { - color: $color-red; - font-style: italic; -} diff --git a/assets/scss/admin/_quiz-attempt-review.scss b/assets/scss/admin/_quiz-attempt-review.scss deleted file mode 100644 index a4b7d85f41..0000000000 --- a/assets/scss/admin/_quiz-attempt-review.scss +++ /dev/null @@ -1,23 +0,0 @@ -.llms-remarks { - - .llms-remarks-field { - height: 120px; - width: 100%; - } - - input[type="number"] { - width: 60px; - } - - -} - - -button[name="llms_quiz_attempt_action"] { - .save { display: none; } - &.grading { - .default { display: none }; - .save { display: inline; } - } -} - diff --git a/assets/scss/admin/_reporting.scss b/assets/scss/admin/_reporting.scss deleted file mode 100644 index 936119a424..0000000000 --- a/assets/scss/admin/_reporting.scss +++ /dev/null @@ -1,341 +0,0 @@ -.llms-reporting.wrap { - - .llms-options-page-contents { - background: #fff; - box-shadow: 0 1px 3px rgba( 0, 0, 0, .13 ); - margin: 0 0 20px; - padding: 20px; - .llms-nav-tab-wrapper.llms-nav-secondary { - background: #fafafa; - box-shadow: none; - margin: 0 -20px 40px; - padding: 0 20px; - .llms-nav-link { - padding: 8px 14px; - } - } - } - - .llms-stab-title { - color: $color-brand-dark-blue; - font-size: 36px; - font-weight: 300; - margin-bottom: 20px; - } - - td.id a { - text-decoration: none; - } - - th.name, td.name, - th.title, td.title { text-align: left; } - - td.section-title { - background: #eaeaea; - text-align: left; - font-weight: 700; - padding: 16px 4px; - } - - td.questions-table { - text-align: left; - - .correct, - .question, - .selected { - text-align: left; - max-width: 300px; - - img { - height: auto; - max-width: 64px; - } - } - } - - table.quiz-attempts { - margin-bottom: 40px; - } - - &.tab--enrollments, - &.tab--sales { - - .llms-nav-tab-wrapper.llms-nav-secondary { - margin-bottom: 0; - } - - .llms-options-page-contents { - box-shadow: none; - background: none; - margin-top: 20px; - padding: 0; - } - - .llms-nav-item.llms-analytics-form { - color: #414141; - font-size: 13px; - padding: 6px 14px; - - input { - border: 0; - font-size: 13px; - margin: 0; - padding: 3px 6px; - vertical-align: middle; - } - - .select2-container { - input { - width: 100% !important; - } - } - } - - .button.small { - height: 23px; - line-height: 23px; - } - - - .llms-analytics-filters { - display: none; - - .llms-nav-item { - box-sizing: border-box; - width: 100%; - - label { - display: block; - } - } - - .button { - float: right; - } - - } - } - - .llms-reporting-tab.llms-reporting-quiz .llms-table-filter-wrap { - width: 160px; - } - - -} - - -.llms-reporting-tab { - - h1, h2, h3, h4, h5, h6 { - margin: 0; - a { - color: $color-brand-dark-blue; - text-decoration: none; - &:hover { - color: $color-brand-blue; - } - } - } - - - .llms-reporting-header { - - background: #fafafa; - padding: 20px; - margin: 0 -20px; - - .llms-reporting-header-img { - border-radius: 50%; - display: inline-block; - margin-right: 10px; - overflow: hidden; - vertical-align: middle; - img { - display: block; - max-height: 64px; - width: auto; - } - } - - .llms-reporting-header-info { - display: inline-block; - vertical-align: middle; - - } - - } - -} - -.llms-reporting-breadcrumbs { - background: #fafafa; - margin: -20px -20px 0; - padding: 20px 20px 10px; - a { - color: $color-brand-blue; - text-decoration: none; - &:hover { - color: $color-brand-blue-dark; - } - &:after { - content: ' > '; - color: #555; - } - - &:last-child { - color: $color-brand-dark-blue; - &:after { display: none;} - } - } -} - -#llms-students-table .name { - text-align: left; -} - -.llms-reporting-tab-content { - display: flex; - - > header { - @include clearfix; - } - - h3 { - margin-bottom: 20px; - } - - .llms-reporting-tab-filter { - float: right; - position: relative; - margin-right: 0.75em; - width: 180px; - top: -3px; - } - - - .llms-reporting-tab-main { - flex: 3; - } - .llms-reporting-tab-side { - flex: 1; - margin-left: 20px; - } - - > .llms-table-wrap { - flex: 1; - } - -} - - -.llms-reporting-widgets { - @include clearfix; -} - -.llms-reporting-widget { - - border-top: 4px solid $color-brand-blue; - background: #fafafa; - margin-bottom: 0.75em; - padding: 18px 15px 15px; - @include clearfix; - - .fa { - color: #555; - float: left; - font-size: 32px; - margin-right: 10px; - } - - .llms-reporting-widget-data { - line-height: 0.8; - } - - strong { - color: #333; - font-size: 20px; - } - - &.llms-reporting-student-address { - strong { - line-height: 1.1; - } - } - - sup, - .llms-price-currency-symbol { - font-size: 75%; - position: relative; - top: -4px; - vertical-align: baseline; - } - - small { - color: #888; - &.compare { - margin-left: 5px; - &.positive { - color: $color-green; - } - &.negative { - color: $color-red; - } - } - } -} - - -.llms-reporting-event { - border-left: 4px solid #555; - background: #fafafa; - font-size: 11px; - line-height: 1.2; - margin-bottom: 0.75em; - padding: 10px; - @include clearfix; - - &.color--blue { - border-left-color: $color-blue; - } - - &.color--green, - &._enrollment_trigger, - &._is_complete.yes { - border-left-color: $color-green; - } - - &.color--purple, - &._status.enrolled { - border-left-color: $color-purple; - } - - &.color--red, - &._status.expired, - &._status.cancelled { - border-left-color: $color-red; - } - &.color--orange, - &._achievement_earned, - &._certificate_earned, - &._email_sent { - border-left-color: $color-orange; - } - - time { - color: #888; - } - - .llms-student-avatar { - margin-left: 10px; - float: right; - } - - a { - text-decoration: none; - color: inherit; - } - -} - -@import "../_includes/quiz-result-question-list"; - diff --git a/assets/scss/admin/_settings.scss b/assets/scss/admin/_settings.scss deleted file mode 100644 index 09d2f3e490..0000000000 --- a/assets/scss/admin/_settings.scss +++ /dev/null @@ -1,174 +0,0 @@ -.wrap.llms-reporting, -.wrap.lifterlms-settings { - - .llms-header { - background: #fff; - border-bottom: 1px solid #efefef; - margin: -10px -20px 0; - padding: 10px 0; - z-index: 1; - - .lifterlms-logo { - max-width: 140px; - } - - .llms-save { - float: right; - } - - .llms-inside-wrap { - padding: 0 10px; - } - - } - - .llms-inside-wrap { - box-sizing: border-box; - max-width: 1000px; - margin: 0 auto; - } - - .llms-nav-tab-wrapper.llms-nav-secondary { - background: #fff; - box-shadow: 0 1px 3px rgba( 0, 0, 0, .13 ); - margin: 0 -20px 20px; - z-index: 1; - - .llms-nav-item { - .llms-nav-link:hover, - &.llms-active .llms-nav-link { - background: #fafafa; - color: $color-blue; - border-top-color: $color-blue; - } - - &.llms-active .llms-nav-link { - font-weight: 700; - } - } - - .llms-nav-link { - border-top: 2px solid transparent; - padding: 14px; - } - - } - - .llms-setting-group { - - background: #fff; - box-shadow: 0 1px 3px rgba( 0, 0, 0, .13 ); - margin: 0 0 20px; - padding: 20px; - - .llms-label { - border-bottom: 1px solid #efefef; - font-weight: 700; - font-size: 20px; - padding: 20px; - margin: -20px -20px 20px; - } - - .llms-help-tooltip .dashicons { - color: #444; - cursor: help; - } - - .form-table { - margin: 0; - tr:first-child .llms-subtitle { - margin-top: 0; - } - } - - td[colspan="2"] { - padding-top: 0; - padding-left: 0; - } - - tr.llms-disabled-field { - opacity: 0.5; - pointer-events: none; - } - - input[type="text"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"], - select, - textarea:not(.wp-editor-area) { - width: 50%; - &.medium { width: 30%; } - &.small { width: 20%; } - &.tiny { width: 10%; } - } - } - - @media only screen and (min-width: 782px) { - .llms-nav-tab-wrapper.llms-nav-secondary { - .llms-nav-item { - .llms-nav-link:hover, - &.llms-active .llms-nav-link { - background: #fff; - } - } - } - } - - // Email Delivery providers. - #llms-mailhawk-connect { - font-size: 16px; - height: auto; - margin: 0 0 6px; - padding: 8px 14px; - position: relative; - - .dashicons { - margin: -4px 4px 0 0; - vertical-align: middle; - } - } - #llms-sendwp-connect { - font-size: 16px; - height: auto; - margin: 0 0 6px; - padding: 8px 14px; - position: relative; - - .fa { - margin-right: 4px; - } - } - -} - -@media only screen and (min-width: 782px) { - .wrap.lifterlms-settings { - .llms-header { - position: sticky; - top: 30px; - } - .llms-nav-tab-wrapper.llms-nav-secondary { - position: sticky; - top: 93px; - } - } -} - -.wrap.llms-reporting { - .llms-inside-wrap { - box-sizing: border-box; - max-width: 1000px; - margin: 0 0 0 20px; - } -} diff --git a/assets/scss/admin/_tabs.scss b/assets/scss/admin/_tabs.scss deleted file mode 100644 index c184ac2c03..0000000000 --- a/assets/scss/admin/_tabs.scss +++ /dev/null @@ -1,103 +0,0 @@ -.llms-nav-tab-wrapper { - background: $color-blue; - margin: 20px 0; - - &.llms-nav-secondary { - background: #e1e1e1; - - .llms-nav-item { - margin: 0; - - .llms-nav-link:hover, - &.llms-active .llms-nav-link { - background: darken( #e1e1e1, 8 ); - } - - } - - .llms-nav-link { - color: #414141; - font-size: 13px; - padding: 8px 14px; - - .dashicons { - font-size: 15px; - height: 15px; - width: 15px; - } - } - - } - - &.llms-nav-text { - background: inherit; - .llms-nav-item { - background: inherit; - &:last-child:after { - display: none; - } - &:after { - content: '|'; - display: inline-block; - margin: 0 3px 0 0; - } - .llms-nav-link:hover, - &.llms-active .llms-nav-link { - background: inherit; - color: $color-brand-blue; - text-decoration: underline; - } - .llms-nav-link { - color: $color-brand-dark-blue; - display: inline-block; - letter-spacing: 0; - margin: 0; - padding: 0; - text-transform: none; - } - } - } - - .llms-nav-items { - @include clearfix; - margin: 0; - } - - .llms-nav-item { - margin: 0; - - .llms-nav-link:hover { - background: $color-brand-blue-dark; - } - &.llms-active .llms-nav-link { - background: $color-brand-dark-blue; - } - - &.llms-active .llms-nav-link { - font-weight: 400; - } - - @media only screen and (min-width: 768px) { - float: left; - - &.llms-nav-item-right { - float: right; - } - } - - } - - .llms-nav-link { - - color: #fff; - cursor: pointer; - display: block; - font-weight: 300; - font-size: 14px; - padding: 9px 18px; - text-align: center; - text-decoration: none; - transition: all .3s ease; - - } -} diff --git a/assets/scss/admin/_wp-menu.scss b/assets/scss/admin/_wp-menu.scss deleted file mode 100644 index ba81cd6235..0000000000 --- a/assets/scss/admin/_wp-menu.scss +++ /dev/null @@ -1,21 +0,0 @@ -#adminmenu { - - .toplevel_page_lifterlms .wp-menu-image img { - padding-top: 6px; - width: 20px; - } - - .toplevel_page_lifterlms, - .opensub .wp-submenu li.current, - .wp-submenu li.current, - .wp-submenu li.current, - .wp-submenu li.current, - a.wp-has-current-submenu:focus+.wp-submenu li.current { - a[href*="page=llms-add-ons"] { - color: $color-orange; - } - } - -} - - diff --git a/assets/scss/admin/breakpoints/_1030up.scss b/assets/scss/admin/breakpoints/_1030up.scss deleted file mode 100644 index 2c94494dcd..0000000000 --- a/assets/scss/admin/breakpoints/_1030up.scss +++ /dev/null @@ -1,76 +0,0 @@ -/****************************************************************** - -Desktop Stylesheet - -******************************************************************/ - -//option page tab menu -.llms-nav-tab { - display: inline-block; - width: 33.333%; -} -.llms-nav-tab-settings { - display: inline-block; - width: 25%; -} - -//select box form wrapper -#llms-form-wrapper { - .llms-select { - display: inline-block; - width: 47.5%; - &:first-child { - margin-right: 5%; - } - - }.llms-filter-options { - display: inline-block; - width: 47.5%; - - &.date-filter { - margin-right: 5%; - }.llms-date-select { - margin-bottom: 0; - } - - }.llms-date-select { - width: 47.5%; - - &:first-child { - margin-right: 5% - } - - } -} - -.llms-widget-row { - @include clearfix; - .llms-widget-1-5 { - vertical-align: top; - width: 20%; - float: left; - box-sizing: border-box; - padding: 0 5px; - } - .llms-widget-1-4 { - vertical-align: top; - width: 25%; - float: left; - box-sizing: border-box; - padding: 0 5px; - } - .llms-widget-1-3 { - width: 33.3%; - float: left; - box-sizing: border-box; - padding: 0 5px; - } - .llms-widget-1-2 { - width: 50%; - float: left; - box-sizing: border-box; - padding: 0 5px; - vertical-align: top; - } - -} diff --git a/assets/scss/admin/breakpoints/_1240up.scss b/assets/scss/admin/breakpoints/_1240up.scss deleted file mode 100644 index 2b3457079d..0000000000 --- a/assets/scss/admin/breakpoints/_1240up.scss +++ /dev/null @@ -1,11 +0,0 @@ -/****************************************************************** - -large Monitor Stylesheet - -******************************************************************/ - -.llms-nav-tab-filters, -.llms-nav-tab-settings { - float: left; - width: 12.5%; -} diff --git a/assets/scss/admin/breakpoints/_481up.scss b/assets/scss/admin/breakpoints/_481up.scss deleted file mode 100644 index 05e7bc2f76..0000000000 --- a/assets/scss/admin/breakpoints/_481up.scss +++ /dev/null @@ -1,15 +0,0 @@ -/****************************************************************** - -Larger Phones - -******************************************************************/ - -//select box form wrapper -#llms-form-wrapper { - - .llms-checkbox { - width: 33%; - //text-align: center; - - } -} diff --git a/assets/scss/admin/breakpoints/_768up.scss b/assets/scss/admin/breakpoints/_768up.scss deleted file mode 100644 index be119130ce..0000000000 --- a/assets/scss/admin/breakpoints/_768up.scss +++ /dev/null @@ -1,73 +0,0 @@ -/****************************************************************** - -Tablets and small computers - -******************************************************************/ - -ul.tabs li{ - display: inline-block; - } - -//option page tab menu -.llms-nav-tab { - display: inline-block; - width: 33%; -} -.llms-nav-tab-settings { - display: inline-block; - width: 25%; -} - -//select box form wrapper -#llms-form-wrapper { - .llms-select { - width: 50%; - max-width: 500px; - - }.llms-filter-options { - width: 50%; - //display: inline-block; - max-width: 500px; - - }.llms-date-select { - width: 47.5%; - - &:first-child { - margin-right: 5% - } - - } -} - -.llms-widget { - input[type="text"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"], - select, - textarea, { - width: 50%; - - &.medium { width: 30%; } - &.small { width: 20%; } - &.tiny { width: 10%; } - } - - // .form-table th { - // width: 140px; - // } - -} - - - diff --git a/assets/scss/admin/breakpoints/_base.scss b/assets/scss/admin/breakpoints/_base.scss deleted file mode 100644 index 4e196a12a8..0000000000 --- a/assets/scss/admin/breakpoints/_base.scss +++ /dev/null @@ -1,93 +0,0 @@ -/****************************************************************** - -Base Mobile - -******************************************************************/ - -.llms-nav-tab, -.llms-nav-tab-filters { - display: block; - width: 100%; -} - -form.llms-nav-tab-filters.full-width { - width: 100%; - - label { - display: inline-block; - width: 10%; - text-align: left; - } - - .select2-container { - width: 85% !important; - } -} - -.llms-nav-tab-settings { - display: block; - width: 100%; -} - -//select box form wrapper -#llms-form-wrapper { - .llms-select { - width: 100%; - margin-bottom: 20px; - - }.llms-checkbox { - display: inline-block; - width: 100%; - text-align: left; - - }.llms-filter-options { - width: 100%; - //margin-bottom: 20px; - - }.llms-date-select { - width: 100%; - display: inline-block; - margin-bottom: 20px; - input[type="text"] { - width: 100%; - } - - }.llms-search-button { - //display: inline-block; - //width: 30%; - #llms-search-button { - - //float: right; - } - - } - -} - -// .llms-widget-full { -// &.top { -// margin-top: 20px; -// } -// } -// .llms-widget { -// .form-table td { -// padding: 15px 0; -// ul { margin: 5px 0 0; } - - -// .conditional-field { -// display: none; -// margin-left: 25px; -// } -// .conditional-radio:checked ~ .conditional-field { -// display: block; -// } - - -// } -// } - -ul.tabs li{ - display: block; - } - diff --git a/assets/scss/admin/metaboxes/_builder-launcher.scss b/assets/scss/admin/metaboxes/_builder-launcher.scss deleted file mode 100644 index 2704fe7895..0000000000 --- a/assets/scss/admin/metaboxes/_builder-launcher.scss +++ /dev/null @@ -1,5 +0,0 @@ -.llms-builder-launcher { - .llms-button-primary { - box-sizing: border-box; - } -} diff --git a/assets/scss/admin/metaboxes/_llms-metabox.scss b/assets/scss/admin/metaboxes/_llms-metabox.scss deleted file mode 100644 index 8fa2649ac2..0000000000 --- a/assets/scss/admin/metaboxes/_llms-metabox.scss +++ /dev/null @@ -1,199 +0,0 @@ - -// This is a "legacy" rule that may be removable -.llms-mb-list { - - label { - font-weight: bold; - width: 100%; - display: block; - } - - .input-full { - width: 100%; - } -} - - -#poststuff .llms-metabox { - - @extend %cf; - - h2, h3, h6 { - font-weight: 300; - margin: 0; - padding: 0; - } - - h2 { - font-size: 22px; - } - - h3 { - color: #777; - font-size: 16px; - } - - h4 { - border-bottom: 1px solid #e5e5e5; - padding: 0; - margin: 0; - } - - .llms-transaction-test-mode { - background: #ffffd7; - font-style: italic; - left: 0; - padding: 2px; - position: absolute; - right: 0; - top: 0; - text-align: center; - } - - a.llms-editable, - .llms-metabox-icon, - button.llms-editable { - color: $color-grey; - text-decoration: none; - &:hover { - color: $color-brand-blue; - } - } - - button.llms-editable { - border: none; - background: none; - cursor: pointer; - padding: 0; - vertical-align: top; - } - - h4 button.llms-editable { - float: right; - } - -} - -.llms-metabox-section { - background: #fff; - margin-top: 25px; - position: relative; - - &.no-top-margin { - margin-top: 0; - } - - .llms-metabox-field { - margin: 15px 0; - position: relative; - label { - color: #777; - display: block; - margin-bottom: 5px; - font-weight: 500; - vertical-align: baseline; - } - - select, - textarea, - input[type="text"], - input[type="number"] { - width: 100%; - } - - input.md-text { - width: 105px; - } - - input.sm-text { - width: 45px; - } - - - .llms-datetime-field { - - .llms-date-input { - width: 95px; - } - .llms-time-input { - width: 45px; - } - em { - font-style: normal; - padding: 0 3px; - } - - } - - } - - -} - -.llms-collapsible { - - @extend %clearfix; - - border: 1px solid #e5e5e5; - position: relative; - margin-top: 0; - margin-bottom: -1px; - - &:last-child { - margin-bottom: 0; - } - - &.opened .llms-collapsible-header { - .dashicons-arrow-down { - display: none; - } - .dashicons-arrow-up { - display: inline; - } - } - - .llms-collapsible-header { - @extend %clearfix; - padding: 10px; - - h3 { - color: #777; - margin: 0; - font-size: 16px; - } - - .dashicons-arrow-up { - display: inline; - } - .dashicons-arrow-up { - display: none; - } - - a { - text-decoration: none; - } - - .dashicons { - color: #777; - cursor: pointer; - transition: color .4s ease; - &:hover { - color: $color-blue; - } - - &.dashicons-warning,&.dashicons-warning:hover, - &.dashicons-trash:hover, - &.dashicons-no:hover { - color: $color-danger; - } - } - - } - - .llms-collapsible-body { - @extend %clearfix; - display: none; - padding: 10px; - } - -} diff --git a/assets/scss/admin/metaboxes/_metabox-field-repeater.scss b/assets/scss/admin/metaboxes/_metabox-field-repeater.scss deleted file mode 100644 index 439a4e6849..0000000000 --- a/assets/scss/admin/metaboxes/_metabox-field-repeater.scss +++ /dev/null @@ -1,37 +0,0 @@ -// ahhhhhhhh -.llms-mb-container .tab-content ul:not(.select2-selection__rendered).llms-mb-repeater-fields > li.llms-mb-list { - border-bottom: none; - padding: 0 0 10px; -} - -.llms-mb-list.repeater { - - .llms-repeater-rows { - position: relative; - margin-top: 10px; - min-height: 10px; - - &.dragging { - background: #efefef; - box-shadow: inset 0 0 0 1px #e5e5e5; - } - } - - .llms-repeater-row { - background: #fff; - } - - .llms-mb-repeater-fields { - - } - - .llms-mb-repeater-footer { - text-align: right; - margin-top: 20px; - } - - .tmce-active .wp-editor-area { - color: #32373c; // wp core default color - } - -} diff --git a/assets/scss/admin/metaboxes/_metabox-instructors.scss b/assets/scss/admin/metaboxes/_metabox-instructors.scss deleted file mode 100644 index f03294ceed..0000000000 --- a/assets/scss/admin/metaboxes/_metabox-instructors.scss +++ /dev/null @@ -1,9 +0,0 @@ -._llms_instructors_data.repeater { - .llms-repeater-rows .llms-repeater-row:first-child { - .llms-repeater-remove { display: none; } - } - - .llms-mb-list { - padding: 0 5px !important; - } -} diff --git a/assets/scss/admin/metaboxes/_metabox-orders.scss b/assets/scss/admin/metaboxes/_metabox-orders.scss deleted file mode 100644 index 3f4a603064..0000000000 --- a/assets/scss/admin/metaboxes/_metabox-orders.scss +++ /dev/null @@ -1,62 +0,0 @@ -.post-type-llms_order #post-body-content { display: none; } -#lifterlms-order-details { - .handlediv, - .handlediv.button-link, - .postbox-header { display: none;} - .inside { - padding: 20px; - margin-top: 0; - - } -} - -// failed transaction color -.llms-table tbody tr.llms-txn-failed td { - background-color: rgba( $color-red, 0.5 ); - border-bottom-color: rgba( $color-red, 0.5 ); -} - -// refunded transaction color -.llms-table tbody tr.llms-txn-refunded td { - background-color: rgba( orange, 0.5 ); - border-bottom-color: rgba( orange, 0.5 ); -} - -.llms-txn-refund-form, -.llms-manual-txn-form { - .llms-metabox-section { - margin-top: 0; - } - .llms-metabox-field { - text-align: right; - input { - &[type="number"] { max-width: 100px; } - &[type="text"] { max-width: 340px; } - - } - } -} - -.llms-manual-txn-form { - background-color: #eaeaea; - .llms-metabox-section { - background-color: #eaeaea; - } -} - -#llms-remaining-edit { - display: none; -} -.llms-remaining-edit--content { - label, span, textarea { - display: block; - } - - label { - margin-bottom: 20px; - } - - textarea, input { - width: 100%; - } -} diff --git a/assets/scss/admin/metaboxes/_metabox-product.scss b/assets/scss/admin/metaboxes/_metabox-product.scss deleted file mode 100644 index 62544c37f8..0000000000 --- a/assets/scss/admin/metaboxes/_metabox-product.scss +++ /dev/null @@ -1,72 +0,0 @@ -.llms-metabox { - - #llms-new-access-plan-model { - display: none; - } - - .llms-access-plans { - @extend %clearfix; - margin-top: 25px; - - > .llms-no-plans-msg { display: none; } - > .llms-no-plans-msg:last-child { - box-shadow: inset 0 0 0 1px #e5e5e5; - display: block; - text-align: center; - padding: 10px; - } - - &.dragging { - background: #efefef; - box-shadow: inset 0 0 0 1px #e5e5e5; - } - - .llms-spinning { - z-index: 1; - } - - .llms-invalid { - border-color: $color-danger; - .dashicons-warning { - display: inline; - } - } - - .dashicons-warning { - display: none; - } - - } - - .llms-access-plan { - - text-align: left; - - [data-tip]:before { - text-align: center; - } - - .llms-plan-link, - [data-controller] { - display: none; - } - - &:hover, - &.opened { - .llms-plan-link { - display: inline-block; - } - } - - .llms-metabox-field { - margin: 5px 0; - } - - .llms-required { - color: $color-danger; - margin-left: 3px; - } - - } - -} diff --git a/assets/scss/admin/metaboxes/_metabox-students.scss b/assets/scss/admin/metaboxes/_metabox-students.scss deleted file mode 100644 index 7b0aef36ef..0000000000 --- a/assets/scss/admin/metaboxes/_metabox-students.scss +++ /dev/null @@ -1,15 +0,0 @@ -.llms-metabox-students { - .llms-table { - tr .name { - text-align: left; - } - } - - .llms-add-student:hover { - color: $color-green; - } - .llms-remove-student:hover { - color: $color-red; - } - -} diff --git a/assets/scss/admin/modules/_forms.scss b/assets/scss/admin/modules/_forms.scss deleted file mode 100644 index fac4a4b803..0000000000 --- a/assets/scss/admin/modules/_forms.scss +++ /dev/null @@ -1,190 +0,0 @@ -/****************************************************************** - -Form Styles - -******************************************************************/ - -// lifterlms form wrapper -#llms-form-wrapper { - - // setup defaults - input[type="text"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"], - input[type="checkbox"], - select, - textarea, - .llms-field { - - // a focused input (or hovered on) - &:focus, - &:active { - - } // end hover or focus - } - - // sub wrapper for search filter form (analytics) - .llms-search-form-wrapper { - border-bottom: 1px solid $color-grey; - margin: 20px 0; - - } - - - #llms_analytics_search { - border:none !important; - text-shadow: none !important; - border: none !important; - outline: none !important; - box-shadow: none !important; - margin: 0 !important; - color: $color-white !important; - background: $color-blue !important; - border-radius: 0; - transition: .5s; - - &:hover { - background: $color-darkblue !important; - - }&:active { - background: $color-lightblue !important; - } - } - -} // end input defaults - - -#llms-skip-setup-form { - .llms-admin-link { - background:none!important; - border:none; - padding:0!important; - color:#0074a2; - cursor:pointer; - &:hover { - color:#2ea2cc - }&:focus{ - color:#124964; - } - - } - -} - -/** - * Toggle Switch ( replaces checkbox on admin panels ) - */ -.llms-switch { - position: relative; - - .llms-toggle { - position: absolute; - margin-left: -9999px; - visibility: hidden; - } - - .llms-toggle + label { - box-sizing: border-box; - display: block; - position: relative; - cursor: pointer; - outline: none; - user-select: none; - } - - input.llms-toggle-round + label { - border: 2px solid #6c7781; - border-radius: 10px; - height: 20px; - width: 36px; - } - input.llms-toggle-round + label:before, - input.llms-toggle-round + label:after { - box-sizing: border-box; - content: ''; - display: block; - position: absolute; - transition: background 0.4s; - } - - input.llms-toggle-round:checked + label { - border-color: #11a0d2; - background-color: #11a0d2; - } - - // Primary dot (that moves.) - input.llms-toggle-round + label:after { - height: 12px; - left: 2px; - top: 2px; - background-color: #6c7781; - border-radius: 50%; - transition: margin 0.4s; - width: 12px; - z-index: 3; - } - - // Primary dot when toggle on. - input.llms-toggle-round:checked + label:after { - background-color: $color-white; - margin-left: 16px; - } - - // Secondary dot: empty on the right side of the toggle when toggled off. - input.llms-toggle-round + label:before { - height: 8px; - top: 4px; - border: 1px solid #6c7781; - border-radius: 50%; - right: 4px; - width: 8px; - z-index: 2; - } - - input.llms-toggle-round:checked + label:before { - border-color: $color-white; - border-radius: 0; - left: 6px; - right: auto; - width: 2px; - } - -} - -#llms-profile-fields { - margin: 50px 0; - .llms-form-field { - padding-left: 0; - } - label { - display: block; - font-weight: bold; - padding: 8px 0 2px; - } - .type-checkbox .type-checkbox { - label { - display: initial; - font-weight: initial; - padding: 0; - } - input { - display: inline-block; - margin-bottom: 0; - width: 1rem; - } - } - select { - max-width: 100%; - } -} diff --git a/assets/scss/admin/modules/_icons.scss b/assets/scss/admin/modules/_icons.scss deleted file mode 100644 index b937c1072c..0000000000 --- a/assets/scss/admin/modules/_icons.scss +++ /dev/null @@ -1,92 +0,0 @@ -/****************************************************************** - -SVG Styles - -******************************************************************/ - -svg { - &.icon { - height: 24px; - width: 24px; - display: inline-block; - fill: currentColor; // Inherit color - vertical-align: baseline; // Options: baseline, sub, super, text-top, text-bottom, middle, top, bottom - - // Different styling for when an icon appears in a button element - button & { - height: 18px; - width: 18px; - margin: 4px -4px 0 4px; - filter: drop-shadow( 0 1px #eee ); - float: right; - - }&.menu-icon { - height: 20px; - width: 20px; - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; - margin-right: 10px; - - }&.tree-icon { - height: 13px; - width: 13px; - vertical-align: middle; - - }&.section-icon { - height: 16px; - width: 16px; - vertical-align: text-bottom; - - }&.button-icon { - height: 16px; - width: 16px; - vertical-align: text-bottom; - - }&.button-icon-attr { - height: 10px; - width: 10px; - vertical-align: middle; - - }&.list-icon { - height: 12px; - width: 12px; - vertical-align: middle; - - &.on { - color: $color-blue; - - }&.off { - color: $color-cinder; - } - - }&.detail-icon { - height: 16px; - width: 16px; - vertical-align: text-bottom; - cursor:default; - - &.on { - color: $color-blue; - - }&.off { - color: $color-lightgrey; - } - } - - } - - &.icon-ion {} - - &.icon-ion-edit {} - - // rotate for arrow tips - &.icon-ion-arrow-up { - transform: rotate(90deg); - } - - use { - pointer-events: none; - } - -} \ No newline at end of file diff --git a/assets/scss/admin/modules/_llms-order-note.scss b/assets/scss/admin/modules/_llms-order-note.scss deleted file mode 100644 index b055ebb144..0000000000 --- a/assets/scss/admin/modules/_llms-order-note.scss +++ /dev/null @@ -1,35 +0,0 @@ -.llms-order-note { - - .llms-order-note-content { - background: #efefef; - margin-bottom: 10px; - padding: 10px; - position: relative; - &:after { - border-style: solid; - border-color: #efefef transparent; - border-width: 10px 10px 0 0; - bottom: -10px; - content: ''; - display: block; - height: 0; - left: 20px; - position: absolute; - width: 0; - - } - p { - font-size: 13px; - margin: 0; - line-height: 1.5; - } - } - - .llms-order-note-meta { - color: #999; - font-size: 11px; - margin-left: 10px; - } - - -} diff --git a/assets/scss/admin/modules/_mb-tabs.scss b/assets/scss/admin/modules/_mb-tabs.scss deleted file mode 100644 index d409d9b635..0000000000 --- a/assets/scss/admin/modules/_mb-tabs.scss +++ /dev/null @@ -1,58 +0,0 @@ -/****************************************************************** - -Metabox Tabs - -******************************************************************/ - -// free space up if the metabox is on the side -#side-sortables .tab-content { - padding: 0; -} - -.llms-mb-container .tab-content{ - display: none; - background: $color-white; - padding: 15px; - - ul:not(.select2-selection__rendered) { - margin: 0; - - > li { - padding: 20px 0; - margin: 0; - border-bottom: 1px solid $color-lightgrey; - - &.select:not([class*="d-"]) { - width: 100%; - } - - &:last-child { - border: 0; - padding-bottom: 0; - - } - - &.top { - border-bottom: 0; - padding-bottom: 0; - } - - } - } - - .full-width { width: 100%; } - - #wp-content-editor-tools { - background: none; - } - -} - -.llms-mb-container .tab-content.llms-active{ - display: inherit; -} - - -.llms-mb-container .tab-content .no-border { - border-bottom: 0px; -} diff --git a/assets/scss/admin/modules/_merge-codes.scss b/assets/scss/admin/modules/_merge-codes.scss deleted file mode 100644 index dc31a9789e..0000000000 --- a/assets/scss/admin/modules/_merge-codes.scss +++ /dev/null @@ -1,58 +0,0 @@ -.button.llms-merge-code-button { - vertical-align: middle; - img { - margin-right: 3px; - margin-top: -3px; - vertical-align: middle; - } -} - -.llms-mb-container { - .button.llms-merge-code-button img { margin-right: 4px; } - .llms-merge-code-wrapper { - float: right; - top: -5px; - } -} - -.llms-merge-code-wrapper { - display: inline; - position: relative; -} - -.llms-merge-codes { - background: #f7f7f7; - border: 1px solid #ccc; - border-radius: 3px; - box-shadow: 0 1px 0 #ccc; - color: #555; - display: none; - left: 1px; - overflow: hidden; - position: absolute; - top: 30px; - width: 160px; - - ul { - margin: 0; - padding: 0; - } - - li { - cursor: pointer; - margin: 0; - padding: 4px 8px !important; - border-bottom: 1px solid #ccc; - } - - li:hover { - color: #23282d; - background: #fefefe; - } - - &.active { - display: block; - z-index: 777; - } - -} diff --git a/assets/scss/admin/modules/_top-modal.scss b/assets/scss/admin/modules/_top-modal.scss deleted file mode 100644 index 9a118bcebc..0000000000 --- a/assets/scss/admin/modules/_top-modal.scss +++ /dev/null @@ -1,203 +0,0 @@ -/****************************************************************** - -Styles for topModal modal - -******************************************************************/ - -/** - * Base modal styles - */ -.topModal { - display:none; - position:relative; - border:4px solid #808080; - background:#fff; - z-index:1000001; - padding:2px; - max-width:500px; - margin: 34px auto 0; - box-sizing: border-box; - box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); - background-color: #ffffff; - border-radius: 2px; - border: 1px solid #dddddd; - -}.topModalClose { - float:right; - cursor:pointer; - margin-right: 10px; - margin-top: 10px; - -}.topModalContainer { - display: none; - overflow: auto; - overflow-y: hidden; - position: fixed; - top: 0 !important; - right: 0; - bottom: 0; - left: 0; - -webkit-overflow-scrolling: touch; - width: auto !important; - margin-left: 0 !important; - background-color: transparent !important; - z-index: 100002 !important; - -}.topModalBackground { - display:none; - background:#000; - position:fixed; - height:100%; - width:100%; - top:0 !important; - left:0; - margin-left: 0 !important; - z-index: 100002 !important; - box-sizing: border-box; - overflow: auto; - overflow-y: hidden; - -}body.modal-open { - overflow: hidden; - -}.llms-modal-header { - border-top-right-radius: 1px; - border-top-left-radius: 1px; - background: $color-blue; - color: #eeeeee; - padding: 10px 15px; - font-size: 18px; - -}#llms-icon-modal-close { - width:16px; - height: 16px; - fill: $color-white; - -}.llms-modal-content { - padding: 20px; - - h3 { - margin-top: 0; - } - -} - -/** - * Custom Modal Styles for LifterLMS - */ -.llms-modal-form { - - h1 { - margin-top: 0; - } - - input[type=text] { - width: 100%; - } - - textarea, - input[type="text"], - input[type="password"], - input[type="file"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"] { - padding: 0 .4em 0 .4em; - margin-bottom: 2em; - vertical-align: middle; - border-radius: 3px; - min-width: 50px; - max-width: 635px; - width: 100%; - min-height: 32px; - background-color: #fff; - border: 1px solid $color-lightgrey; - margin: 0 0 24px 0; - outline: none; - transition: border 0.3s ease-in-out 0s; - - &:focus { - background: $color-white; - border: 1px solid $color-blue; - - } - } - - textarea { - padding: .4em !important; - height: 100px !important; - border-radius: 3px; - vertical-align: middle; - min-height: 32px; - outline: none; - box-sizing: border-box; - - &:focus { - background: $color-white; - border: 1px solid $color-blue; - - } - - } - - .chosen-container-single .chosen-single { - border-radius: 3px; - vertical-align: middle; - min-height: 32px; - border: 1px solid $color-lightgrey; - width: 100%; - background: $color-white !important; - outline: none; - box-sizing: border-box; - box-shadow: 0 0 0 #fff; - line-height: 32px; - margin: 0 0 24px 0; - - &:focus { - background: $color-white; - border: 1px solid $color-blue; - } - } - - .chosen-container-single .chosen-single div b { - margin-top: 4px; - } - - .chosen-search input[type=text] { - border: 1px solid $color-lightgrey; - - &:focus { - background-color: $color-white; - border: 1px solid $color-blue; - } - - } - - .chosen-container-single .chosen-drop { - margin-top: -28px; - } - - .llms-button-primary, .llms-button-secondary { - padding: 10px 10px; - border-radius: 0; - transition: .5s; - box-shadow: 0 1px 1px #ccc; - - &.full { - width: 100%; - } - } -} - -.modal-open .select2-dropdown { - z-index: 1000005; -} diff --git a/assets/scss/admin/modules/_voucher.scss b/assets/scss/admin/modules/_voucher.scss deleted file mode 100644 index 7262f52e43..0000000000 --- a/assets/scss/admin/modules/_voucher.scss +++ /dev/null @@ -1,133 +0,0 @@ -a.llms-voucher-delete { - background: $color-danger; - color: $color-white; - display: block; - padding: 4px 2px; - text-decoration: none; - transition: ease .3s all; - - &:hover { - background: #af3a26; - } -} - - - -.llms-voucher-codes-wrapper, -.llms-voucher-redemption-wrapper { - - table { - width: 100%; - border-collapse: collapse; - - th, td { - border: none; - } - - thead { - background-color: $color-blue; - color:#fff; - th { - padding: 10px 10px; - } - } - - tr { - counter-increment: row-counter; - &:nth-child(even) { - background-color: #F1F1F1; - } - - td { - padding: 5px; - &:first-child:before { - content: counter( row-counter ); - } - } - } - } -} - -.llms-voucher-codes-wrapper { - - table { - width: 100%; - border-collapse: collapse; - - th, td { - border: none; - } - - thead { - background-color: $color-blue; - color:#fff; - } - - tr { - &:nth-child(even) { - background-color: #F1F1F1; - } - - td { - - span { - display: inline-block; - min-width: 30px; - } - } - } - } - - button { - cursor: pointer; - } - - .llms-voucher-delete { - float: right; - margin-right: 15px; - } - - .llms-voucher-uses { - width: 50px; - } - - .llms-voucher-add-codes { - float: right; - - input[type="text"] { - width: 30px; - } - } -} - -.llms-voucher-export-wrapper { - - .llms-voucher-export-type { - width: 100%; - - p { - margin: 0 0 0 15px; - } - } - - .llms-voucher-email-wrapper { - width: 100%; - margin: 25px 0; - - input[type="text"] { - width: 100%; - } - - p { - margin: 0; - } - } - - > button { - float: right; - } -} - -.postbox .inside { - overflow: auto; -} diff --git a/assets/scss/admin/modules/_widgets.scss b/assets/scss/admin/modules/_widgets.scss deleted file mode 100644 index a3ef13b305..0000000000 --- a/assets/scss/admin/modules/_widgets.scss +++ /dev/null @@ -1,160 +0,0 @@ -.llms-widget { - background: $color-white; - box-sizing: border-box; - margin-bottom: 20px; - padding: 10px 20px; - position: relative; - width: 100%; - - &.alt { - - border: 1px solid $color-lightgrey; - background-color: #efefef; - margin-bottom: 10px; - - .llms-label { - color: #777; - font-size: 14px; - margin-bottom: 10px; - padding-bottom: 5px; - } - - h2 { - color: #444; - font-weight: 300; - } - - } - - h1 { - font-size: 2.4em; - } - - h2 { - font-size: 1.8em; - } - - .llms-label { - border-bottom: 1px solid $color-lightgrey; - box-sizing: border-box; - color: $color-grey; - font-size: 18px; - font-weight: 300; - letter-spacing: 1px; - margin: 0 0 15px; - padding-bottom: 10px; - text-align: center; - text-transform: uppercase; - } - - .llms-chart { - width: 100%; - padding: 10px; - box-sizing: border-box; - } - - mark.yes { - background-color: #7ad03a; - } - - .llms-subtitle { - margin-bottom: 0; - } - - .spinner { - float: none; - left: 50%; - margin: -10px 0 0 -10px; - position: absolute; - top: 50%; - z-index: 2; - } - - &.is-loading { - - &:before { - background: $color-white; - bottom: 0; - content: ''; - left: 0; - opacity: 0.9; - position: absolute; - right: 0; - top: 0; - z-index: 1; - } - - .spinner { - visibility: visible; - } - - } - - td[colspan="2"] { - padding-left: 0; - } - - tr.llms-disabled-field { - opacity: 0.5; - pointer-events: none; - } - -} - -.llms-widget-1-3, -.llms-widget-1-4, -.llms-widget-1-5 { - text-align: center; -} - - -.llms-widget { - .llms-widget-info-toggle { - color: $color-lightgrey; - cursor: pointer; - font-size: 16px; - position: absolute; - right: 25px; - top: 13px; - } - - &.info-showing { - .llms-widget-info { - display: block; - } - } -} -.llms-widget-info { - background: $color-cinder; - color: $color-white; - bottom: -50px; - display: none; - padding: 15px; - position: absolute; - text-align: center; - left: 10px; - right: 15px; - z-index: 3; - &:before { - content: ''; - border: 12px solid transparent; - border-bottom-color: $color-cinder; - left: 50%; - margin-left: -12px; - position: absolute; - top: -24px; - } - - p { - margin: 0; - } - -} - -.llms-widget-row { - @include clearfix(); -} - -.llms-widget-row .no-padding { - padding: 0 !important; -} diff --git a/assets/scss/admin/partials/_grid.scss b/assets/scss/admin/partials/_grid.scss deleted file mode 100644 index add6846568..0000000000 --- a/assets/scss/admin/partials/_grid.scss +++ /dev/null @@ -1,276 +0,0 @@ -/****************************************************************** - -Grids for Breakpoints - -******************************************************************/ - -// using a mixin since we can't use placeholder selectors -@mixin grid-col { - float: left; - padding-right: 0.75em; - box-sizing: border-box; - -} - -// the last column -.last-col { - float: right; - padding-right: 0 !important; -} -.last-col:after { - clear: both; -} - -/* -Mobile Grid Styles -These are the widths for the mobile grid. -There are four types, but you can add or customize -them however you see fit. -*/ -@media (max-width: 767px) { - - .m-all { - @include grid-col; - width: 100%; - padding-right: 0; - } - - .m-1of2 { - @include grid-col; - width: 50%; - } - - .m-1of3 { - @include grid-col; - width: 33.33%; - } - - .m-2of3 { - @include grid-col; - width: 66.66%; - } - - .m-1of4 { - @include grid-col; - width: 25%; - } - - .m-3of4 { - @include grid-col; - width: 75%; - } - - .m-right { - text-align: center; - } - .m-center { - text-align: center; - } - .m-left { - text-align: center; - } - - .d-right { - text-align: right; - } - .d-center { - text-align: center; - } - .d-left { - text-align: left; - } - -} // end mobile styles - - -/* Portrait tablet to landscape */ -@media (min-width: 768px) and (max-width: 1029px) { - - .t-all { - @include grid-col; - width: 100%; - padding-right: 0; - } - - .t-1of2 { - @include grid-col; - width: 50%; - } - - .t-1of3 { - @include grid-col; - width: 33.33%; - } - - .t-2of3 { - @include grid-col; - width: 66.66%; - } - - .t-1of4 { - @include grid-col; - width: 25%; - } - - .t-3of4 { - @include grid-col; - width: 75%; - } - - .t-1of5 { - @include grid-col; - width: 20%; - } - - .t-2of5 { - @include grid-col; - width: 40%; - } - - .t-3of5 { - @include grid-col; - width: 60%; - } - - .t-4of5 { - @include grid-col; - width: 80%; - } - - .d-right { - text-align: right; - } - .d-center { - text-align: center; - } - .d-left { - text-align: left; - } - -} // end tablet - -/* Landscape to small desktop */ -@media (min-width: 1030px) { - - .d-all { - @include grid-col; - width: 100%; - padding-right: 0; - } - - .d-1of2 { - @include grid-col; - width: 50%; - } - - .d-1of3 { - @include grid-col; - width: 33.33%; - } - - .d-2of3 { - @include grid-col; - width: 66.66%; - } - - .d-1of4 { - @include grid-col; - width: 25%; - } - - .d-3of4 { - @include grid-col; - width: 75%; - } - - .d-1of5 { - @include grid-col; - width: 20%; - } - - .d-2of5 { - @include grid-col; - width: 40%; - } - - .d-3of5 { - @include grid-col; - width: 60%; - } - - .d-4of5 { - @include grid-col; - width: 80%; - } - - .d-1of6 { - @include grid-col; - width: 16.6666666667%; - } - - .d-1of7 { - @include grid-col; - width: 14.2857142857%; - } - - .d-2of7 { - @include grid-col; - width: 28.5714286%; - } - - .d-3of7 { - @include grid-col; - width: 42.8571429%; - } - - .d-4of7 { - @include grid-col; - width: 57.1428572%; - } - - .d-5of7 { - @include grid-col; - width: 71.4285715%; - } - - .d-6of7 { - @include grid-col; - width: 85.7142857%; - } - - .d-1of8 { - @include grid-col; - width: 12.5%; - } - - .d-1of9 { - @include grid-col; - width: 11.1111111111%; - } - - .d-1of10 { - @include grid-col; - width: 10%; - } - - .d-1of11 { - @include grid-col; - width: 9.09090909091%; - } - - .d-1of12 { - @include grid-col; - width: 8.33%; - } - - .d-right { - text-align: right; - } - .d-center { - text-align: center; - } - .d-left { - text-align: left; - } - -} // end desktop styles diff --git a/assets/scss/admin/post-tables/_llms_orders.scss b/assets/scss/admin/post-tables/_llms_orders.scss deleted file mode 100644 index 27bf992fb0..0000000000 --- a/assets/scss/admin/post-tables/_llms_orders.scss +++ /dev/null @@ -1,7 +0,0 @@ -.wp-list-table { - @include order_status_badges(); -} - -#lifterlms-order-transactions .llms-table tfoot th { - text-align: right; -} diff --git a/assets/scss/admin/post-tables/_post-tables.scss b/assets/scss/admin/post-tables/_post-tables.scss deleted file mode 100644 index f3843b0916..0000000000 --- a/assets/scss/admin/post-tables/_post-tables.scss +++ /dev/null @@ -1,6 +0,0 @@ -.llms-post-table-post-filter { - display: inline-block; - margin-right: 6px; - max-width: 100%; - width: 220px; -} diff --git a/assets/scss/builder.scss b/assets/scss/builder.scss deleted file mode 100644 index f6341b9f07..0000000000 --- a/assets/scss/builder.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import "_includes/vars"; -@import "_includes/vars-brand-colors"; -@import "_includes/extends"; -@import "_includes/mixins"; - -@import "admin/course-builder"; diff --git a/assets/scss/certificates.scss b/assets/scss/certificates.scss deleted file mode 100644 index c9fd4b6ee9..0000000000 --- a/assets/scss/certificates.scss +++ /dev/null @@ -1,138 +0,0 @@ -body { - background-color: #fff; - background-image: none; - margin: 0 auto; -} - -// .header { -// display: none; -// } - -// #content { -// background: none; -// } - -// .entry { -// top: 120px; -// width: 100%; -// margin-bottom: 40px !important; -// background: none; -// } - -// .hentry, -// .llms_certificate.hentry, -// .llms_my_certificate.hentry { -// margin-bottom: 40px !important; -// padding: 50px 90px !important; -// background: none; -// border: none; -// } - -.header, .footer, -.wrap-header, .wrap-footer, -.site-header, .site-footer, -.nav-primary, .primary-nav { - display: none; -} - -// .llms-certificate-container { -// position: relative; -// padding: 20px; -// margin: auto; -// margin-bottom: 20px; -// -webkit-print-color-adjust: exact; -// overflow: hidden; -// } - -.llms-certificate-container h1:first-child { - text-align: center; -} - -.llms-print-certificate { - margin-top: 40px; - text-align: center; - - form { - display: inline; - } -} - -.llms-certificate-container { - - margin: 0 auto; - padding: 0; - overflow: hidden; - - .certificate-background { - position: relative; - z-index: 1; - width: 100%; - display: block; - // position: absolute; - // z-index: 1; - // top: 0; - // left: 0; - // right: 0; - // margin: 0 auto; - } - - .llms_certificate, - .llms_my_certificate { - margin: 80px; - position: relative; - // top: -100%; - // width: 100%; - z-index: 2; - } - -} - - - -@media print { - - @page { size: auto; } - - .no-print { - display: none; - } - - /* Make everything on the page invisible */ - body * { - visibility: hidden !important; - background: #fff none; - } - - .site, .site-content { - overflow: visible; - } - - /* remove all headers, menus and footers */ - header, aside, nav, footer { - display: none !important; - } - - /* make sure a .container parent doesn't shift the certificate see: https://github.com/gocodebox/lifterlms/issues/1163 */ - .single-llms_my_certificate .container, - .single-llms_certificate .container { - width: 100%; - } - - /* make only the certificate container and its children visible */ - .llms-certificate-container, - .llms-certificate-container * { - visibility: visible !important; - background: transparent none; - } - - /* position certificate absolutely and center horizontally */ - .llms-certificate-container { - position: absolute; - top: 0; - left: 0; - right: 0; - margin: 0 auto; - background: #fff none; - } - -} diff --git a/assets/scss/frontend/_checkout.scss b/assets/scss/frontend/_checkout.scss deleted file mode 100644 index 1ac62a5fbe..0000000000 --- a/assets/scss/frontend/_checkout.scss +++ /dev/null @@ -1,184 +0,0 @@ -.llms-checkout-wrapper { - form.llms-login { - border: 3px solid $color-brand-blue; - display: none; - margin-bottom: 10px; - } - .llms-form-heading { - background: $color-brand-blue; - color: #fff; - margin: 0 0 5px; - padding: 10px; - } -} - -.llms-checkout { - background: #fff; - position: relative; -} - -.llms-checkout-cols-2 { - @extend %clearfix; - - @media all and (min-width: 800px) { - - .llms-checkout-col { - float: left; - - &.llms-col-1 { - margin-right: 5px; - width: calc( 58% - 5px ); - } - &.llms-col-2 { - margin-left: 5px; - width: calc( 42% - 5px ); - - button { - width: 100%; - } - } - } - - } - -} - - .llms-checkout-section { - border: 3px solid $color-brand-blue; - margin-bottom: 10px; - position: relative; - } - - .llms-checkout-section-content { - margin: 10px; - &.llms-form-fields { - margin: 0px; - } - - .llms-label { - font-weight: 400; - font-variant: small-caps; - text-transform: lowercase; - } - - .llms-order-summary { - list-style-type: none; - margin: 0; - padding: 0; - - li { list-style-type: none; } - - li.llms-pricing { - &.on-sale, - &.has-coupon { - .price-regular { text-decoration: line-through; } - } - } - - - } - - .llms-coupon-wrapper { - border-top: 1px solid #dadada; - margin-top: 10px; - padding-top: 10px; - - .llms-coupon-entry { - display: none; - margin-top: 10px; - } - } - - } - - .llms-form-field.llms-payment-gateway-option { - - label + span.llms-description { - display: inline-block; - margin-left: 5px; - } - - .llms-description { - a { - border: none; - box-shadow: none; - text-decoration: none; - } - img { - display: inline; - max-height: 22px; - vertical-align: middle; - } - } - - } - - .llms-checkout-wrapper ul.llms-payment-gateways { - margin: 5px 0 0; - padding: 0; - } - ul.llms-payment-gateways { - list-style-type: none; - - li:last-child:after { - border-bottom: 1px solid #dadada; - content: ''; - display: block; - margin: 10px; - } - - .llms-payment-gateway { - margin-bottom: 5px; - list-style-type: none; - &:last-child { - margin-bottom: none; - } - - &.is-selected { - .llms-payment-gateway-option label { - font-weight: 700; - } - .llms-gateway-fields { - display: block; - - .llms-notice { - margin-left: 10px; - margin-right: 10px; - } - } - } - - .llms-form-field { - padding-bottom: 0; - } - } - - .llms-gateway-description { - margin-left: 40px; - } - - .llms-gateway-fields { - display: none; - margin: 5px 0 20px; - } - - .llms-payment-gateway-error { - padding: 0 10px; - } - } - - .llms-checkout-confirm { - margin: 0 10px; - } - - .llms-payment-method { - margin: 10px 10px 0; - } - - .llms-gateway-description { - p { - font-size: 85%; - font-style: italic; - margin-bottom: 0; - } - } diff --git a/assets/scss/frontend/_course.scss b/assets/scss/frontend/_course.scss deleted file mode 100644 index 2296f78086..0000000000 --- a/assets/scss/frontend/_course.scss +++ /dev/null @@ -1,21 +0,0 @@ -.course { - .llms-meta-info { - margin: 20px 0; - .llms-meta-title { - margin-bottom: 5px; - } - .llms-meta { - p { - margin-bottom: 0; - } - span { - font-weight: 700; - } - } - } - .llms-course-progress { - margin: 40px auto; - max-width: 480px; - text-align: center; - } -} diff --git a/assets/scss/frontend/_llms-access-plans.scss b/assets/scss/frontend/_llms-access-plans.scss deleted file mode 100644 index b8f3eda2a2..0000000000 --- a/assets/scss/frontend/_llms-access-plans.scss +++ /dev/null @@ -1,192 +0,0 @@ -.llms-access-plans { - @extend %clearfix; - - @media all and (min-width: 600px) { - $cols: 1; - @while $cols <= 5 { - &.cols-#{$cols} .llms-access-plan { - width: 100% / $cols; - } - $cols: $cols + 1; - } - } - -} - -.llms-free-enroll-form { - margin-bottom: 0; -} - -.llms-access-plan { - box-sizing: border-box; - float: left; - text-align: center; - width: 100%; - - .llms-access-plan-footer, - .llms-access-plan-content { - background: #f1f1f1; - } - - &.featured { - - .llms-access-plan-featured { - background: lighten( $color-brand-blue, 8 ); - } - - .llms-access-plan-footer, - .llms-access-plan-content { - border-left: 3px solid $color-brand-blue; - border-right: 3px solid $color-brand-blue; - } - - .llms-access-plan-footer { - border-bottom-color: $color-brand-blue; - } - - } - - &.on-sale { - .price-regular { text-decoration: line-through; } - } - - .stamp { - background: $color-brand-blue; - color: #fff; - font-size: 11px; - font-style: normal; - font-weight: 300; - padding: 2px 3px; - vertical-align: top; - } - - .llms-access-plan-restrictions ul { margin: 0; } - -} - .llms-access-plan-featured { - color: #fff; - font-size: 14px; - font-weight: 400; - margin: 0 2px 0 2px; - } - - .llms-access-plan-content { - margin: 0 2px 0; - - .llms-access-plan-pricing { - padding: 10px 0 0; - } - } - - .llms-access-plan-title { - background: $color-brand-blue; - color: #fff; - margin-bottom: 0; - padding: 10px; - } - - .llms-access-plan-pricing { - - .llms-price-currency-symbol { - font-size: 14px; - vertical-align: top; - } - - } - - .llms-access-plan-price { - font-size: 18px; - font-variant: small-caps; - line-height: 20px; - - .lifterlms-price { - font-weight: 700; - } - - &.sale { - padding: 5px 0; - border-top: 1px solid #d0d0d0; - border-bottom: 1px solid #d0d0d0; - } - } - - .llms-access-plan-trial, - .llms-access-plan-schedule, - .llms-access-plan-sale-end, - .llms-access-plan-expiration { - font-size: 15px; - font-variant: small-caps; - line-height: 1.2; - } - - .llms-access-plan-description { - font-size: 16px; - padding: 10px 10px 0; - - ul { - margin: 0; - li { - border-bottom: 1px solid #d0d0d0; - list-style-type: none; - &:last-child { - border-bottom: none; - } - } - } - - div, img, p, ul, li { - &:last-child { margin-bottom: 0; } - } - } - - .llms-access-plan-restrictions { - .stamp { - vertical-align: baseline; - } - ul { - margin: 0; - li { - font-size: 12px; - line-height: 14px; - list-style-type: none; - } - } - a { - color: $color-brand-orange; - &:hover { - color: $color-brand-orange-dark; - } - } - } - - .llms-access-plan-footer { - border-bottom: 3px solid #f1f1f1; - padding: 10px; - margin: 0 2px 2px 2px; - - .llms-access-plan-pricing { - padding: 0 0 10px; - } - } - - -.webui-popover-content .llms-members-only-restrictions { - text-align: center; - ul,ol,li,p { - margin: 0; - padding: 0; - } - ul,ol,li { - list-style-type: none; - } - li { - padding: 8px 0; - border-top: 1px solid #3b3b3b; - &:first-child { - border-top: none; - } - a { - display: block; - } - } -} diff --git a/assets/scss/frontend/_llms-achievements-certs.scss b/assets/scss/frontend/_llms-achievements-certs.scss deleted file mode 100644 index 3999faae21..0000000000 --- a/assets/scss/frontend/_llms-achievements-certs.scss +++ /dev/null @@ -1,106 +0,0 @@ -ul.llms-achievements-loop, -.lifterlms ul.llms-achievements-loop, -ul.llms-certificates-loop, -.lifterlms ul.llms-certificates-loop { - - @include clearfix(); - list-style-type: none; - margin: 0 -10px; - padding: 0; - - li.llms-achievement-loop-item, - li.llms-certificate-loop-item { - box-sizing: border-box; - display: block; - float: left; - list-style-type: none; - margin: 0; - padding: 10px; - width: 100%; - } - - @media all and (min-width: 600px) { - $cols: 1; - @while $cols <= 5 { - &.loop-cols-#{$cols} li.llms-achievement-loop-item, - &.loop-cols-#{$cols} li.llms-certificate-loop-item { - width: 100% / $cols; - } - $cols: $cols + 1; - } - } - -} - -.llms-achievement, -.llms-certificate { - - background: #f1f1f1; - border: none; - color: inherit; - display: block; - text-decoration: none; - width: 100%; - - &:hover { - background: #eaeaea; - } - - .llms-achievement-img { - display: block; - margin: 0; - width: 100%; - } - - .llms-achievement-title { - font-size: 16px; - margin: 0; - padding: 10px; - text-align: center; - } - - .llms-certificate-title { - font-size: 16px; - margin: 0; - padding: 0 0 10px; - } - - .llms-achievement-info, - .llms-achievement-date { - display: none; - } - - .llms-achievement-content { - padding: 20px; - &:empty { - padding: 0; - } - *:last-child { - margin-bottom: 0; - } - } - -} - -.llms-certificate { - border: 4px double #f1f1f1; - padding: 20px 10px; - background: #fff; - text-align: center; - &:hover { - background: #fff; - border-color: #eaeaea; - } -} - -.llms-achievement-modal { - .llms-achievement { - background: #fff; - } - .llms-achievement-info { - display: block; - } - .llms-achievement-title { - display: none; - } -} diff --git a/assets/scss/frontend/_llms-author.scss b/assets/scss/frontend/_llms-author.scss deleted file mode 100644 index 5f92792c91..0000000000 --- a/assets/scss/frontend/_llms-author.scss +++ /dev/null @@ -1,62 +0,0 @@ -.llms-author { - .name { - margin-left: 5px; - } - .label { - margin-left: 5px; - } - .avatar { - border-radius: 50%; - } - .bio { - margin-top: 5px; - } -} - - -.llms-instructor-info { - .llms-instructors { - - .llms-col { - &:first-child .llms-author { - margin-left: 0; - } - &:last-child .llms-author { - margin-right: 0; - } - } - - .llms-author { - - background: #f5f5f5; - border-top: 4px solid $color-brand-blue; - text-align: center; - margin: 45px 5px 5px; - padding: 0 10px 10px; - - .avatar { - background: $color-brand-blue; - border: 4px solid $color-brand-blue; - display: block; - margin: -35px auto 10px; - } - - .llms-author-info { - display: block; - // margin: 0 0 5px; - &.name { - font-weight: 700; - } - &.label { - font-size: 85%; - } - &.bio { - font-size: 90%; - margin-bottom: 0; - } - } - } - - } - -} diff --git a/assets/scss/frontend/_llms-notifications.scss b/assets/scss/frontend/_llms-notifications.scss deleted file mode 100644 index 8a1da8d02e..0000000000 --- a/assets/scss/frontend/_llms-notifications.scss +++ /dev/null @@ -1,177 +0,0 @@ -.llms-notification { - - @include clearfix(); - - background: #fff; - box-shadow: 0 1px 2px -2px #333, 0 1px 1px -1px #333; - border-top: 4px solid $color-blue; - left: 12px; - opacity: 0; - padding: 12px; - position: fixed; - right: 12px; - top: 24px; - transition: - opacity 0.4s ease-in-out, - right 0.4s ease-in-out, - ; - visibility: none; - width: auto; - z-index: 9999999; - - &.visible { - opacity: 1; - transition: - opacity 0.4s ease-in-out, - right 0.4s ease-in-out, - top 0.1s ease-in-out, - background 0.2s ease-in-out, - transform 0.2s ease-in-out - ; - visibility: visible; - - &:hover { - .llms-notification-dismiss { - opacity: 1; - } - } - - } - - .llms-notification-content { - align-items: center; - display: flex; - - } - - .llms-notification-main { - align-self: flex-start; - flex: 4; - order: 2; - } - - .llms-notification-title { - font-size: 18px; - margin: 0; - } - - .llms-notification-body { - font-size: 14px; - line-height: 1.4; - p, li { - font-size: inherit; - } - p { - margin-bottom: 8px; - } - - .llms-mini-cert { - background: #f6f6f6; - border: 4px double #b0b0b0; - padding: 24px 8px; - .llms-mini-cert-title { - font-size: 16px; - font-weight: 700; - margin-bottom: 8px; - text-align: center; - } - p,li { - font-size: 14px; - &:last-child { margin-bottom: 0; } - } - } - } - - .llms-notification-aside { - align-self: flex-start; - flex: 1; - margin-right: 12px; - order: 1; - } - - .llms-notification-icon { - display: block; - max-width: 64px; - } - - .llms-notification-footer { - border-top: 1px solid #e5e5e5; - font-size: 12px; - margin-top: 12px; - padding: 6px 6px 0; - text-align: right; - } - - .llms-notification-dismiss { - color: $color-danger; - cursor: pointer; - font-size: 22px; - position: absolute; - right: 10px; - top: 8px; - transition: opacity 0.4s ease-in-out; - } - -} - -.llms-sd-notification-center { - - .llms-notification-list, - .llms-notification-list-item { - list-style-type: none; - margin: 0; - padding: 0; - } - - .llms-notification-list-item { - &:hover .llms-notification { - background: #fcfcfc; - } - } - - .llms-notification { - opacity: 1; - border-top: 1px solid #e5e5e5; - left: auto; - padding: 24px; - position: relative; - right: auto; - top: auto; - visibility: visible; - width: auto; - .llms-notification-aside { - max-width: 64px; - } - .llms-notification-footer { - border: none; - padding: 0; - text-align: left; - } - .llms-progress { - display: none !important; - } - .llms-notification-date { - color: #515151; - float: left; - margin-right: 6px; - } - .llms-mini-cert { - margin: 0 auto; - max-width: 380px; - } - } -} - -@media all and (min-width: 480px) { - .llms-notification { - left: auto; - right: -800px; - width: 360px; - &.visible { - right: 24px; - } - .llms-notification-dismiss { - opacity: 0; - } - } -} diff --git a/assets/scss/frontend/_llms-outline-collapse.scss b/assets/scss/frontend/_llms-outline-collapse.scss deleted file mode 100644 index ad109277a6..0000000000 --- a/assets/scss/frontend/_llms-outline-collapse.scss +++ /dev/null @@ -1,39 +0,0 @@ -.llms-widget-syllabus--collapsible { - - .llms-section { - - .section-header { - - cursor: pointer; - - } - - &.llms-section--opened { - - .llms-collapse-caret { - .fa-caret-right { display: none; } - } - - } - - &.llms-section--closed { - - .llms-collapse-caret { - .fa-caret-down { display: none; } - } - - .llms-lesson { - display: none; - } - - } - - } - - .llms-syllabus-footer { - - text-align: left; - - } - -} diff --git a/assets/scss/frontend/_llms-pagination.scss b/assets/scss/frontend/_llms-pagination.scss deleted file mode 100644 index d0585c70a2..0000000000 --- a/assets/scss/frontend/_llms-pagination.scss +++ /dev/null @@ -1,29 +0,0 @@ -.llms-pagination { - - ul { - list-style-type: none; - @extend %cf; - - li { - - float: left; - - a { - border-bottom: 0; - text-decoration: none; - } - - .page-numbers { - padding: 0.5em; - text-decoration: underline; - - &.current { - text-decoration: none; - } - } - - } - - } - -} \ No newline at end of file diff --git a/assets/scss/frontend/_llms-progress.scss b/assets/scss/frontend/_llms-progress.scss deleted file mode 100644 index a43869179d..0000000000 --- a/assets/scss/frontend/_llms-progress.scss +++ /dev/null @@ -1,32 +0,0 @@ -/* progress bar */ -.llms-progress { - clear: both; - display: flex; - flex-direction: row-reverse; - position: relative; - height: 1em; - width: 100%; - margin: 15px 0; -} - -.llms-progress .llms-progress-bar { - background-color: #f1f2f1; - position: relative; - height: .4em; - top: .3em; - width: 100%; -} - -.llms-progress .progress-bar-complete { - background-color: $color-brand-pink; - height: 100%; -} - -.progress__indicator { - float: right; - text-align: right; - height: 1em; - line-height: 1em; - margin-left: 5px; - white-space: nowrap; -} diff --git a/assets/scss/frontend/_llms-quizzes.scss b/assets/scss/frontend/_llms-quizzes.scss deleted file mode 100644 index 831da49059..0000000000 --- a/assets/scss/frontend/_llms-quizzes.scss +++ /dev/null @@ -1,343 +0,0 @@ -.single-llms_quiz { - - @import "../_includes/quiz-result-question-list"; - - .llms-return { - margin-bottom: 10px; - } - - .llms-quiz-results { - @include clearfix(); - - .llms-donut { - &.passing { - color: $color-success; - svg path { - stroke: $color-success; - } - } - &.pending { - color: #555; - svg path { - stroke: #555; - } - } - &.failing { - color: $color-danger; - svg path { - stroke: $color-danger; - } - } - } - - .llms-quiz-results-aside, - .llms-quiz-results-main, - .llms-quiz-results-history { - margin-bottom: 20px; - } - - - @media all and (min-width: 600px) { - .llms-quiz-results-aside { - float: left; - width: 220px; - } - .llms-quiz-results-main, - .llms-quiz-results-history { - float: left; - width: calc( 100% - 300px ); - } - } - - } - - ul.llms-quiz-meta-info, - ul.llms-quiz-meta-info li { - list-style-type: none; - margin: 0; - padding: 0 - } - - ul.llms-quiz-meta-info { - margin-bottom: 10px; - } - - .llms-quiz-buttons { - margin-top: 10px; - text-align: left; - - form { display: inline-block; } - } - -} - -.llms-quiz-question-wrapper { - min-height: 140px; - position: relative; - .llms-quiz-loading { - bottom: 20px; - left: 0; - position: absolute; - right: 0; - text-align: center; - z-index: 1; - } -} - -.llms-quiz-ui { - background: #fcfcfc; - padding: 20px; - position: relative; - - .llms-quiz-header { - align-items: center; - display: flex; - margin: 0 0 30px; - } - - .llms-progress { - background-color: #f1f2f1; - flex-direction: row; - height: 8px; - margin: 0; - overflow: hidden; - .progress-bar-complete { - transition: width 0.3s ease-in; - width: 0; - } - } - - .llms-error { - @include clearfix(); - background: $color-danger; - border-radius: 4px; - color: #fff; - margin: 10px 0; - padding: 10px; - - a { - color: rgba( #fff, 0.6 ); - float: right; - font-size: 22px; - line-height: 1; - text-decoration: none; - } - - } - - .llms-quiz-counter { - display: none; - - color: #6a6a6a; - float: right; - font-size: 18px; - - .llms-sep { - margin: 0 5px; - } - } - - .llms-quiz-nav { - margin-top: 20px; - button { - margin: 0 10px 0 0; - } - } - -} - -// single question wrapper -.llms-question-wrapper { - - .llms-question-text { - font-size: 30px; - font-weight: 400; - margin-bottom: 15px; - } - - ol.llms-question-choices { - list-style-type: none; - margin: 0; - padding: 0; - - li.llms-choice { - border-bottom: 1px solid #e8e8e8; - margin: 0; - padding: 0; - position: relative; - - &:last-child { - border-bottom: none; - } - - &.type--picture { - border-bottom: none; - label { - padding: 0; - } - .llms-marker { - bottom: 10px; - margin: 0; - position: absolute; - right: 10px; - } - .llms-choice-image { - margin: 2px; - padding: 20px; - transition: background 0.4s ease; - img { - display: block; - width: 100%; - } - } - input:checked ~ .llms-choice-image { - background: #efefef - } - } - - input { - display: none; - left: 0; - pointer-events: none; - position: absolute; - top: 0; - visibility: hidden; - } - - label { - margin: 0; - padding: 10px 20px; - position: relative; - // &:hover { - &.hovered { - .llms-marker:not(.type--lister) { - .iterator { - display: none; - } - .fa { - display: inline; - } - } - } - } - - .llms-marker { - - background: #f0f0f0; - display: inline-block; - font-size: 20px; - height: 40px; - line-height: 40px; - margin-right: 10px; - text-align: center; - transition: all 0.2s ease; - vertical-align: middle; - width: 40px; - - .fa { - display: none; - } - - &.type--lister, - &.type--checkbox { border-radius: 4px; } - &.type--radio { border-radius: 50%; } - - } - - input:checked + .llms-marker { - background: $color-brand-pink; - color: #fff; - .iterator { - display: none; - } - .fa { - display: inline; - } - } - - .llms-choice-text { - display: inline-block; - font-size: 18px; - font-weight: 400; - line-height: 1.6; - margin-bottom: 0; - vertical-align: middle; - width: calc( 100% - 60px ); - } - - } - } - -} - -.llms-quiz-timer { - background: #fff; - border: 1px solid $color-green; - border-radius: 4px; - color: $color-green; - float: right; - font-size: 18px; - line-height: 1; - margin-left: 20px; - padding: 8px 12px; - position: relative; - white-space: nowrap; - z-index: 1; - - &.color-half { - border-color: $color-orange; - color: $color-orange - } - - &.color-empty { - border-color: $color-danger; - color: $color-danger - } - - .llms-tiles { - display: inline-block; - margin-left: 5px; - } -} - - -// /* My Quizzes */ -// .llms-quiz-results { -// @extend %cf; -// font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif; -// position: relative; -// } -// .llms-quiz-results > h3 { -// background-color: #f5f5f5; -// padding: 4px; -// } - -// .llms-quiz-result-details { -// float: left; -// ul { -// list-style-type: none; -// float: left; -// li { -// list-style-type: none; -// font-size: 20px; -// } -// } -// } -// .llms-attempts { -// font-weight: bold; -// } - -// .llms-pass-perc { -// font-weight: bold; -// } -// .llms-content-block { -// margin: 6px 0; -// } -// .llms-question-wrapper { -// margin: 40px 0 20px 0; -// } -// .llms-question-count { -// margin-bottom: 20px; -// } - - diff --git a/assets/scss/frontend/_llms-table.scss b/assets/scss/frontend/_llms-table.scss deleted file mode 100644 index 5eb2234cd1..0000000000 --- a/assets/scss/frontend/_llms-table.scss +++ /dev/null @@ -1,59 +0,0 @@ -.llms-table { - border: 1px solid #efefef; - width: 100%; - - thead { - th,td { - font-weight: 700; - } - } - - tbody { - tr:nth-child( odd ) { - td, th { - background: #f9f9f9; - } - } - tr:last-child { - border-bottom-width: 0; - } - } - - tfoot { - tr { - background: #f9f9f9; - .llms-pagination .page-numbers { - margin: 0; - } - .llms-table-sort { - text-align: right; - form, select, input, button { - margin: 0; - } - } - } - } - - th { - font-weight: 700; - } - - th, td { - border-bottom: 1px solid #efefef; - padding: 8px 12px; - - // launchpad compat... - &:first-child { padding-left: 12px; } - &:last-child { padding-right: 12px; } - - } - -} - -// launchpad compat... -#page .llms-table tfoot label { - display: inline; -} -#page .llms-table tfoot select { - height: auto; -} diff --git a/assets/scss/frontend/_loop.scss b/assets/scss/frontend/_loop.scss deleted file mode 100644 index 784245d64c..0000000000 --- a/assets/scss/frontend/_loop.scss +++ /dev/null @@ -1,263 +0,0 @@ -.llms-loop-list { - @extend %clearfix; - - list-style: none; - margin: 0 -10px; - padding: 0; - - @media all and (min-width: 600px) { - $cols: 1; - @while $cols <= 6 { - &.cols-#{$cols} .llms-loop-item { - width: 100% / $cols; - } - $cols: $cols + 1; - } - } - - -} - -.llms-loop-item { - float: left; - list-style: none; - margin: 0; - padding: 0; - width: 100%; -} - - - .llms-loop-item-content { - background: #f1f1f1; - padding-bottom: 10px; - margin: 10px; - - &:hover { - background: #eaeaea; - } - - .llms-loop-link { - color: #212121; - display: block; - &:visited { - color: #212121; - } - } - - .llms-featured-image { - display: block; - max-width: 100%; - } - - .llms-loop-title { - margin-top: 5px; - &:hover { - color: $color-brand-blue; - } - } - - .llms-meta, - .llms-author, - .llms-loop-title { - padding: 0 10px; - } - - .llms-meta, - .llms-author { - color: #444; - display: block; - font-size: 13px; - margin-bottom: 3px; - &:last-child { - margin-bottom: 0; - } - } - - .llms-featured-img-wrap { - overflow: hidden; - } - - p { - margin-bottom: 0; - } - - .llms-progress { - margin: 0; - height: .4em; - - .progress__indicator { - display: none; - } - - .llms-progress-bar { - background-color: #f6f6f6; - right: 0; - top: 0; - } - } - - } - - - -// .llms-membership-list .memberships { -// border-top: 1px solid #f6f6f6; -// width: 100%; -// display: inline-block; -// text-align: center; -// list-style: none; -// clear: both; -// margin: 0; -// padding: 0; -// } - - - -// .llms-course-list { - -// .llms-membership-link { -// @extend %llms-element; - -// display: block -// } - -// .llms-membership-footer { -// border-top: 3px solid $color-white; -// margin: 15px -15px 0; -// padding: 15px 15px 0; -// text-align: center; -// } - -// } - - - - -// .llms-membership-list .memberships li { -// width: 300px; -// margin: 15px; -// list-style: none; -// vertical-align: top; -// display: inline-block; -// text-align: left; -// } - -// .llms-membership-list .memberships li.first { -// margin-left: 0; -// } - -// .llms-membership-list .memberships li.last { -// margin-right: 15px; -// } - -// .llms-membership-list .memberships li .llms-title { -// display: block; -// font-weight: 700; -// margin-bottom: .5em; -// font-size: 18px; -// text-decoration: none; -// line-height: 30px; -// } - -// .llms-membership-list .memberships li .llms-price { -// display: block; -// font-weight: 700; -// // margin-bottom: .5em; -// // font-size: 24px; -// text-decoration: none; -// line-height: 30px; -// } - -// .llms-course-list { -// //margin: 30px 0; -// padding: 30px; -// //background: #FFF; -// // border-radius: 2px; -// display: inline-block; -// width: 100%; -// box-sizing: border-box; - -// .llms-course-link { -// @extend %llms-element; - -// display: block -// } - -// .llms-course-footer { -// border-top: 3px solid $color-white; -// margin: 15px -15px 0; -// padding: 15px 15px 0; -// text-align: center; -// } - -// .llms-progress { -// margin-top: 0; -// // .progress-bar { -// // background-color: $color-white; -// // } -// } - -// } - -// .llms-course-list .courses { -// //border-top: 1px solid #f6f6f6; -// width: 100%; -// display: inline-block; -// text-align: center; -// list-style: none; -// clear: both; -// margin: 0; -// padding: 0; -// } - -// .llms-course-list .courses li { -// width: 300px; -// padding-top: 0; // twentyfifteen compat -// margin: 15px; -// list-style: none; -// vertical-align: top; -// display: inline-block; -// text-align: left; -// } -// @media screen and (max-width: $break-small) { -// .llms-course-list { -// padding: 30px 10px; - -// .courses li { -// width: auto; -// } -// } -// } - -// // .llms-course-list .courses li.first { -// // margin-left: 0; -// // } - -// .llms-course-list .courses li.last { -// margin-right: 15px; -// } - -// .llms-course-list .courses li .llms-title { -// display: block; -// font-weight: 700; -// margin-bottom: .5em; -// font-size: 18px; -// text-decoration: none; -// line-height: 30px; -// } - -// .llms-course-list .courses li .llms-price { -// display: block; -// font-weight: 700; -// // margin-bottom: .5em; -// // font-size: 24px; -// text-decoration: none; -// line-height: 30px; -// } - - - - -// .courses a.llms-course-link:hover { -// text-decoration: none; -// } diff --git a/assets/scss/frontend/_main.scss b/assets/scss/frontend/_main.scss deleted file mode 100644 index 26e216b91e..0000000000 --- a/assets/scss/frontend/_main.scss +++ /dev/null @@ -1,480 +0,0 @@ - - - - -.llms-membership-image { - display: block; - margin: 0 auto; -} - - - -.llms-course-image { - display: block; - margin: 0 auto; - max-width: 100%; -} -.llms-featured-image { - display: block; - margin: 0 auto; -} -.llms-image-thumb { - width: 150px; -} - -// Responsive Videos. -.llms-video-wrapper { - - .center-video { - height: auto; - max-width: 100%; - overflow: hidden; - position: relative; - padding-top: 56.25%; - text-align: center; - - & > .wp-video, - .fluid-width-video-wrapper, - iframe, object, embed { - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - } - - & > .wp-video { - width: 100% !important; - } - .fluid-width-video-wrapper { - padding-top: 0 !important; - } - } - -} - - - - - - - - - - - -.clear { - clear: both; - width: 100%; -} -.llms-featured-image { - text-align: center; -} - -/* Genesis Overrides */ -h1, h2, h3, h4, h5, h6 { - font-weight: 300; -} - -#main-content .llms-payment-options p { - margin: 0; - font-size: 16px; -} - -.llms-option { - display: block; - position: relative; - margin: 20px 0; - padding-left:40px; - font-size: 16px; - - label { - cursor: pointer; - position: static; - } -} -.llms-option:first-child { - margin-top:0; -} -.llms-option:last-child { - margin-bottom:0; -} -#main-content .llms-option:last-child { - margin-bottom:0; -} - -.llms-option input[type="radio"] { - display: block; - position: absolute; - top:3px; - left:0; - z-index: 0; -} - -.llms-option input[type="radio"] { - display: inline-block; -} -.llms-option input[type="radio"] + label span.llms-radio { - display: none; -} - -.llms-option input[type="radio"] + label span.llms-radio { - appearance: none; - - z-index: 20; - position: absolute; - top: 0; - left: -2px; - display: inline-block; - width: 24px; - height: 24px; - border-radius: 50%; - cursor: pointer; - vertical-align: middle; - box-shadow: hsla(0,0%,100%,.15) 0 1px 1px, inset hsla(0,0%,0%,.5) 0 0 0 1px; - - background: #efefef; - background-image: radial-gradient(ellipse at center, $color-red 0%,$color-red 40%,#efefef 45%); - background-repeat: no-repeat; - - transition: background-position .15s cubic-bezier(.8, 0, 1, 1); -} -.llms-option input[type="radio"]:checked + label span.llms-radio { - transition: background-position .2s .15s cubic-bezier(0, 0, .2, 1); -} - -.llms-option input[type="radio"] + label span.llms-radio { - background-position: -24px 0; -} -.llms-option input[type="radio"]:checked + label span.llms-radio { - background-position: 0 0; -} - -.llms-option input[type="submit"] { - border:none; - background:$color-red; - color:#fff; - font-size:20px; - padding:10px 0; - border-radius:3px; - cursor:pointer; - width:100%; -} -.llms-styled-text { - padding: 14px 0; -} -.llms-notice-box { - border-radius: 3px; - z-index: 10; - margin: 10px 0; - padding: 15px 20px; - //background: #fffef4; - border: 1px solid #ccc; - list-style-type: none; - width: 100%; - overflow: auto; - input[type="text"] { - height: auto; - } - .col-1-1 { - width: 100%; - input[type=text] { - width: 100%; - } - } - .col-1-2 { - width: 50%; - float: left; - @media screen and (max-width: $break-medium) { - width: 100%; - } - } - .col-1-3 { - width: 33%; - float: left; - margin-right: 10px; - } - .col-1-4 { - width: 25%; - float: left; - margin-right: 10px; - @media screen and (max-width: $break-medium) { - width: 100%; - } - } - .col-1-6 { - width: 16.6%; - float: left; - margin-right: 10px; - } - .col-1-8 { - width: 11%; - float: right; - } - .llms-pad-right { - padding-right: 10px; - @media screen and (max-width: $break-medium) { - padding-right: 0; - } - } -} -input[type="text"].cc_cvv, -#cc_cvv { - margin-right: 0; - width: 23%; - float: right; -} -.llms-clear-box { - border-radius: 3px; - z-index: 10; - margin: 10px 0; - padding: 15px 20px; - list-style-type: none; - width: 100%; - overflow: auto; -} -.llms-price-label { - font-weight: normal; -} -.llms-final-price { - font-weight: bold; - float: right; -} -.llms-center-content { - text-align: center; -} -.llms-input-text { - background-color: #fff; - border: 1px solid #ddd; - color: #333; - font-size: 18px; - font-weight: 300; - padding: 16px; - width: 100%; -} -.llms-price { - margin-bottom: 0; - font-weight: bold; -} -.llms-price-loop { - margin-bottom: 0; - font-weight: bold; -} - -// hentry overrides -.courses .entry { - padding: 0 -} -.list-view .site-content .llms-course-list .hentry, .list-view .site-content .llms-membership-list .hentry { - border-top: 0; - padding-top: 0; -} -.llms-content { - width: 100%; -} - -.llms-lesson-button-wrapper { - width: 100%; - display: block; - clear: both; - text-align: center; -} -.llms-template-wrapper { - width: 100%; - display: block; - clear: both; -} -.llms-button-wrapper { - width: 100%; - display: block; - clear: both; - text-align: center; -} - - -//custom select box -.llms-styled-select { - border: 1px solid #ccc; - box-sizing: border-box; - border-radius: 3px; - overflow: hidden; - position: relative; -} -.llms-styled-select, .llms-styled-select select { - width: 100%; -} -select:focus { outline: none; } -.llms-styled-select select { - height: 34px; - padding: 5px 0 5px 5px; - background: transparent; - border: none; - -webkit-appearance: none; - font-size: 16px; - color: #444444; -} - -@-moz-document url-prefix(){ - .--ms-styled-select select { width: 110%; } -} - -.llms-styled-select .fa-sort-desc { - position: absolute; - top: 0; - right: 12px; - font-size: 24px; - color: #ccc; -} - -select::-ms-expand { display: none; } - -_:-o-prefocus, .selector { - .llms-styled-select { background: none; } -} - -.llms-form-item-wrapper { - margin-bottom: 1em; -} - -/* Circle Graph */ -.llms-progress-circle { - position: relative; - width: 200px; - height: 200px; - float: left; -} - -.llms-progress-circle-count { - top: 27%; - position: absolute; - width: 94%; - text-align: center; - color: #666; - font-size:44px; -} -.llms-progress-circle svg { - position: absolute; - width: 200px; - height: 200px; -} -.llms-progress-circle circle { - fill: transparent; -} -svg .llms-background-circle { - fill: transparent; - stroke-width: 10px; - stroke: #f1f2f1; - stroke-dasharray: 430; -} - -svg .llms-animated-circle { - fill: transparent; - stroke-width: 10px; - stroke: #e5554e; - stroke-dasharray: 430; - stroke-dashoffset: 430 - 20 -} - - - - - - - -.llms-widget-syllabus { - - .llms-lesson.current-lesson .lesson-title { - font-weight: 700; - } - - .llms-lesson-complete, .lesson-complete-placeholder { - font-size: 1.2em; - margin-right: 6px; - color: #ccc; - &.done { - color: #e5554e; - } - }.section-title { - font-weight: bold; - }.lesson-title { - a { - text-decoration: none; - &:hover { - text-decoration: none !important; - } - } - &.done { - a { - color: #999; - text-decoration: line-through; - } - } - } - ul { - list-style-type: none; - li { - list-style-type: none; - ul li { - margin: 0 0 2px 0; - padding: 0; - } - } - } -} - - - -.llms-remove-coupon { - float: right; -} - - - - - -.llms-lesson-link-locked, .llms-lesson-link-locked:hover { - background: #f1f1f1; - box-shadow: 0 1px 2px 0 rgba(1, 1, 1, 0.4); - display: block; - color: #a6a6a6; - min-height: 85px; - padding: 15px; - text-decoration: none; - position: relative; -} - -.llms-lesson-preview.is-complete .llms-lesson-link-locked { - padding-left: 75px; -} - -.llms-lesson-tooltip { display: none; - position:absolute; - color: #000000; - background-color: #c0c0c0; - padding:.25em; - border-radius: 2px; - z-index: 100; - top:0; - left:50%; - text-align: center; - -webkit-transform: translateX(-50%) translateY(-100%); - transform: translateX(-50%) translateY(-100%); - } - -/* arrows - :after */ -.llms-lesson-tooltip:after { - content: ""; - width: 0; - height: 0; - border-top: 8px solid #c0c0c0; - border-left: 8px solid transparent; - border-right: 8px solid transparent; - position:absolute; - bottom:-8px; - left:50%; - transform: translateX(-50%); -} - -.llms-lesson-tooltip.active { - display: inline-block; -} diff --git a/assets/scss/frontend/_notices.scss b/assets/scss/frontend/_notices.scss deleted file mode 100644 index 477078366d..0000000000 --- a/assets/scss/frontend/_notices.scss +++ /dev/null @@ -1,41 +0,0 @@ -.llms-notice { - background: rgba( $color-brand-blue, .3 ); - border-color: $color-brand-blue; - border-style: solid; - border-width: 3px; - padding: 10px; - margin-bottom: 10px; - - p, ul { - &:last-child { margin-bottom: 0; } - } - - li { - list-style-type: none; - } - - &.llms-debug { - background: rgba( #cacaca, .3 ); - border-color: #cacaca; - } - - &.llms-error { - background: rgba( $color-red, .3 ); - border-color: $color-red; - } - - &.llms-success { - background: rgba( $color-green, .3 ); - border-color: $color-green; - } - -} - -// this helps genesis and numerous other themes out a bit -// by being slightly more specific -.entry-content .llms-notice { - margin: 0 0 10px; - li { - list-style-type: none; - } -} diff --git a/assets/scss/frontend/_student-dashboard.scss b/assets/scss/frontend/_student-dashboard.scss deleted file mode 100644 index debab768e0..0000000000 --- a/assets/scss/frontend/_student-dashboard.scss +++ /dev/null @@ -1,342 +0,0 @@ -.llms-student-dashboard { - - .llms-sd-nav {} - - .llms-sd-title { - margin: 25px 0; - } - - .llms-sd-items { // ul - @extend %clearfix; - list-style-type: none; - margin: 0; - padding: 0; - } - .llms-sd-item { // li - float: left; - list-style-type: none; - margin: 0; - padding: 0; - - &:last-child { - .llms-sep { - display: none; - } - } - - .llms-sep { - color: #333; - margin: 0 5px; - } - } - - .llms-sd-section { - margin-bottom: 25px; - .llms-sd-section-footer { - margin-top: 10px; - } - } - - .orders-table { - - border: 1px solid #f5f5f5; - width: 100%; - - thead { - display: none; - th,td { - font-weight: 700; - } - @media all and ( min-width: 600px ) { - display: table-header-group; - } - } - - tbody { - tr:nth-child( even ) { - td, th { - background: #f9f9f9; - } - } - } - - tfoot { - th, td { - padding: 10px; - text-align: right; - &:last-child { border-bottom-width: 0; } - } - } - - th { - font-weight: 700; - } - - th, td { - border-color: #efefef; - border-style: solid; - border-width: 0; - display: block; - padding: 8px 12px; - text-align: center; - - .llms-button-primary { - display: inline-block; - } - - &:last-child { - border-bottom-width: 1px; - } - - &:before { - content: attr( data-label ); - } - - @media all and ( min-width: 600px ) { - border-bottom-width: 1px; - display: table-cell; - text-align: left; - &:first-child { width: 220px; } - &:before { display: none; } - } - - } - - @media all and ( min-width: 600px ) { - &.transactions th:first-child {width: auto; } - } - - } - - @include order_status_badges(); - - .llms-person-form-wrapper { - .llms-change-password { display: none; } - } - - .order-primary { - - @media all and ( min-width: 600px ) { - float: left; - width: 68%; - } - - } - .order-secondary { - - @media all and ( min-width: 600px ) { - float: left; - width: 32%; - } - - form { - margin-bottom: 0; - } - - } - - // stack columns when alternate layout declared via filter - @media all and ( min-width: 600px ) { - .llms-view-order.llms-stack-cols { - .order-primary, - .order-secondary { - float: none; - width: 100%; - } - } - } - - .llms-switch-payment-source { - .llms-notice, - .entry-content .llms-notice { - margin-left: 10px; - margin-right: 10px; - } - } - - .llms-switch-payment-source-main { - border: none; - display: none; - margin: 0; - ul.llms-payment-gateways { - padding: 10px 15px 0; - margin: 0; - } - .llms-payment-method, - ul.llms-order-summary { - padding: 0 25px 10px; - margin: 0; - list-style-type: none; - li { list-style-type: none; } - } - } - - /** - * Dashboard Home - */ - .llms-loop-list { - margin: 0 -10px; - } - -} - -// My Grades course list -.llms-sd-grades { - .llms-table { - .llms-progress { - display: block; - margin: 0; - .llms-progress-bar { - top: 0; - height: 1.4em; - } - .progress__indicator { - font-size: 1em; - position: relative; - right: 0.4em; - top: 0.2em; - z-index: 1; - } - } - } -} - -// grades table for a single course -.llms-table.llms-single-course-grades { - - th { - font-weight: 400; - } - - td { - .llms-donut { - display: inline-block; - vertical-align: middle; - } - .llms-status { - margin-right: 4px; - } - .llms-donut + .llms-status { - margin-left: 4px; - } - } - - th.llms-section_title { - font-size: 110%; - font-weight: 700; - } - - td.llms-lesson_title { - padding-left: 36px; - max-width: 40%; - } - td.llms-associated_quiz { - .llms-donut { - display: inline-block; - margin-right: 5px; - vertical-align: middle; - } - } - td.llms-lesson_title { - a[href="#"] { - pointer-events: none; - } - a[href^="#"] { - color: inherit; - position: relative; - .llms-tooltip { - max-width: 380px; - width: 380px; - &.show { - top: -54px; - } - } - } - } -} - -.llms-sd-widgets { - display: flex; - - .llms-sd-widget { - background: #f9f9f9; - flex: 1; - margin: 10px 10px 20px; - padding: 0 0 20px; - &:first-child { - margin-left: 0; - } - &:last-child { - margin-right: 0; - } - - .llms-sd-widget-title { - background: $color-brand-blue; - color: #fff; - font-size: 18px; - line-height: 1; - margin: 0 0 20px; - padding: 10px; - } - - .llms-sd-widget-empty { - font-size: 14px; - font-style: italic; - opacity: 0.5; - text-align: center; - } - - .llms-donut { - margin: 0 auto; - } - - .llms-sd-date { - opacity: 0.8; - text-align: center; - font-size: 22px; - line-height: 1.1; - span { - display: block; - &.day { - font-size: 52px; - } - &.diff { - font-size: 12px; - font-style: italic; - margin-top: 8px; - opacity: 0.75; - } - } - } - - .llms-achievement { - background: transparent; - margin: 0 auto; - max-width: 120px; - .llms-achievement-title { - display: none; - } - } - - } - - -} - - -.llms-sd-pagination { - margin-top: 24px; - @include clearfix; - .llms-button-secondary { - display: inline-block; - &.prev { float: left; } - &.next { float: right; } - } -} - - -.llms-sd-notification-center { - .llms-notification { - z-index: 1; - } -} diff --git a/assets/scss/frontend/_syllabus.scss b/assets/scss/frontend/_syllabus.scss deleted file mode 100644 index 95c553fbdc..0000000000 --- a/assets/scss/frontend/_syllabus.scss +++ /dev/null @@ -1,147 +0,0 @@ -.llms-syllabus-wrapper { - - margin: 15px; - text-align: center; - - .llms-section-title { - margin: 25px 0 0; - } - -} - -.llms-course-navigation { - @extend %clearfix; - - .llms-prev-lesson, - .llms-next-lesson, - .llms-back-to-course { - width: 49%; - } - - .llms-prev-lesson, - .llms-back-to-course { - float: left; - margin-right: 0.5%; - } - - .llms-next-lesson, - .llms-prev-lesson + .llms-back-to-course { - float: right; - margin-left: 0.5%; - } - -} - -.llms-lesson-preview { - display: inline-block; - margin-top: 15px; - max-width: 100%; - position: relative; - width: 480px; - - .llms-lesson-link { - background: #f1f1f1; - color: #212121; - display: block; - // height: 100%; - padding: 15px; - text-decoration: none; - - @include clearfix(); - - &:hover { - background: #eaeaea; - } - - &:visited { - color: #212121; - } - - } - - .llms-lesson-thumbnail { - margin-bottom: 10px; - img { - display: block; - width: 100%; - } - } - - .llms-pre-text { - text-align: left; - } - - .llms-lesson-title { - font-weight: 700; - margin: 0 auto 10px; - text-align: left; - &:last-child { - margin-bottom: 0; - } - } - - .llms-lesson-excerpt { - text-align: left; - } - - .llms-main { - float: left; - width: 100%; - } - .llms-extra { - float: right; - width: 15%; - } - - .llms-extra + .llms-main { - width: 85%; - } - - .llms-lesson-counter, - .llms-free-lesson-svg, - .llms-lesson-complete, - .llms-lesson-complete-placeholder { - display: block; - font-size: 32px; - margin-bottom: 15px; - } - - &.is-free, - &.is-complete { - .llms-lesson-complete { - color: $color-brand-blue; - } - } - - .llms-icon-free { - background: $color-brand-blue; - border-radius: 4px; - color: #f1f1f1; - display: inline-block; - padding: 5px 6px 4px; - line-height: 1; - font-size: 14px; - } - - &.is-incomplete { - .llms-lesson-complete { - color: #cacaca; - } - } - - .llms-lesson-counter { - font-size: 16px; - line-height: 1; - } - - .llms-free-lesson-svg { - fill: currentColor; - height: 23px; - width: 50px; - } - - p { - margin-bottom: 0; - } - -} diff --git a/assets/scss/frontend/_tooltip.scss b/assets/scss/frontend/_tooltip.scss deleted file mode 100644 index afa87788eb..0000000000 --- a/assets/scss/frontend/_tooltip.scss +++ /dev/null @@ -1,63 +0,0 @@ -.llms-tooltip { - - background: #2a2a2a; - border-radius: 4px; - color: #fff; - font-size: 14px; - line-height: 1.2; - opacity: 0; - top: -20px; - padding: 8px 12px; - left: 50%; - position: absolute; - pointer-events: none; - transform: translateX( -50% ); - transition: opacity .2s ease, top .2s ease; - max-width: 320px; - - &.show { - top: -28px; - opacity: 1; - } - - &:after { - - bottom: -8px; - border-top: 8px solid #2a2a2a; - border-left: 8px solid transparent; - border-right: 8px solid transparent; - content: ''; - height: 0; - left: 50%; - position: absolute; - transform: translateX( -50% ); - width: 0; - - } - -} - - - -.webui-popover-title { - font-size: initial; - font-weight: initial; - line-height: initial; -} -.webui-popover-inverse { - .webui-popover-inner .close { - color: #fff; - opacity: 0.6; - text-shadow: none; - &:hover { - opacity: 0.8; - } - } - .webui-popover-content a { - color: #fff; - text-decoration: underline; - &:hover { - text-decoration: none; - } - } -} diff --git a/assets/scss/frontend/_voucher.scss b/assets/scss/frontend/_voucher.scss deleted file mode 100644 index 6c950121af..0000000000 --- a/assets/scss/frontend/_voucher.scss +++ /dev/null @@ -1,3 +0,0 @@ -.voucher-expand { - display: none; -} \ No newline at end of file diff --git a/assets/scss/lifterlms.scss b/assets/scss/lifterlms.scss deleted file mode 100644 index a91e040adb..0000000000 --- a/assets/scss/lifterlms.scss +++ /dev/null @@ -1,40 +0,0 @@ -// -// Main Frontend CSS File -// - -@import "_includes/vars"; -@import "_includes/extends"; -@import "_includes/grid"; -@import "_includes/mixins"; -@import "_includes/buttons"; -@import "_includes/llms-donut"; -@import "_includes/tooltip"; - -@import "frontend/main"; -@import "frontend/loop"; -@import "frontend/course"; -@import "frontend/syllabus"; -@import "frontend/llms-progress"; -@import "frontend/llms-author"; - -@import "frontend/notices"; -@import "frontend/llms-achievements-certs"; -@import "frontend/llms-notifications"; -@import "frontend/llms-pagination"; -@import "frontend/tooltip"; - - -@import "frontend/llms-quizzes"; - -@import "frontend/voucher"; -@import "frontend/llms-access-plans"; -@import "frontend/checkout"; -@import "_includes/llms-form-field"; - -@import "frontend/llms-outline-collapse"; - -@import "frontend/student-dashboard"; -@import "frontend/llms-table"; - -@import "_includes/vendor/_font-awesome"; -@import "_includes/spinner"; diff --git a/assets/vendor/datetimepicker/jquery.datetimepicker.full.js b/assets/vendor/datetimepicker/jquery.datetimepicker.full.js old mode 100755 new mode 100644 diff --git a/assets/vendor/datetimepicker/jquery.datetimepicker.full.min.js b/assets/vendor/datetimepicker/jquery.datetimepicker.full.min.js old mode 100755 new mode 100644 diff --git a/assets/vendor/datetimepicker/jquery.datetimepicker.min.css b/assets/vendor/datetimepicker/jquery.datetimepicker.min.css old mode 100755 new mode 100644 diff --git a/assets/vendor/izimodal/iziModal.css b/assets/vendor/izimodal/iziModal.css old mode 100755 new mode 100644 diff --git a/assets/vendor/izimodal/iziModal.js b/assets/vendor/izimodal/iziModal.js old mode 100755 new mode 100644 diff --git a/assets/vendor/izimodal/iziModal.min.css b/assets/vendor/izimodal/iziModal.min.css old mode 100755 new mode 100644 diff --git a/assets/vendor/izimodal/iziModal.min.js b/assets/vendor/izimodal/iziModal.min.js old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/animated-overlay.gif b/assets/vendor/jquery-ui-flick/images/animated-overlay.gif old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_flat_0_aaaaaa_40x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_flat_0_aaaaaa_40x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_flat_0_eeeeee_40x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_flat_0_eeeeee_40x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_flat_55_ffffff_40x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_flat_55_ffffff_40x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_flat_75_ffffff_40x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_flat_75_ffffff_40x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_glass_65_ffffff_1x400.png b/assets/vendor/jquery-ui-flick/images/ui-bg_glass_65_ffffff_1x400.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png b/assets/vendor/jquery-ui-flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-icons_0073ea_256x240.png b/assets/vendor/jquery-ui-flick/images/ui-icons_0073ea_256x240.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-icons_454545_256x240.png b/assets/vendor/jquery-ui-flick/images/ui-icons_454545_256x240.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-icons_666666_256x240.png b/assets/vendor/jquery-ui-flick/images/ui-icons_666666_256x240.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-icons_ff0084_256x240.png b/assets/vendor/jquery-ui-flick/images/ui-icons_ff0084_256x240.png old mode 100755 new mode 100644 diff --git a/assets/vendor/jquery-ui-flick/images/ui-icons_ffffff_256x240.png b/assets/vendor/jquery-ui-flick/images/ui-icons_ffffff_256x240.png old mode 100755 new mode 100644 diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 06a3c51389..0000000000 --- a/babel.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Babel config - * - * @package LifterLMS/Dev/Scripts - * - * @since Unknown - * @version Unknown - */ - -const presets = [ [ "@babel/env", ] ]; - -module.exports = { presets }; diff --git a/class-lifterlms.php b/class-lifterlms.php index 10c9c5c612..3c797b986c 100644 --- a/class-lifterlms.php +++ b/class-lifterlms.php @@ -34,7 +34,7 @@ final class LifterLMS { * * @var string */ - public $version = '5.9.0'; + public $version = '5.10.0'; /** * LLMS_Assets instance diff --git a/composer.json b/composer.json deleted file mode 100644 index 4ea91c9337..0000000000 --- a/composer.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "name": "gocodebox/lifterlms", - "description": "LifterLMS, the #1 WordPress LMS solution, makes it easy to create, sell, and protect engaging online courses.", - "keywords": [ - "WordPress", - "LMS" - ], - "homepage": "https://lifterlms.com", - "license": "GPL-3.0+", - "authors": [ - { - "name": "LifterLMS", - "email": "help@lifterlms.com", - "homepage": "https://lifterlms.com" - } - ], - "type": "wordpress-plugin", - "support": { - "forum": "https://wordpress.org/support/plugin/lifterlms", - "issues": "https://github.com/gocodebox/lifterlms/issues", - "source": "https://github.com/gocodebox/lifterlms" - }, - "autoload": { - "psr-4": { - "LLMS\\": "includes" - } - }, - "require": { - "php": ">=7.3", - "composer/installers": "~1.9.0", - "deliciousbrains/wp-background-processing": "1.0.2", - "lifterlms/lifterlms-blocks": "2.3.1", - "lifterlms/lifterlms-cli": "0.0.3", - "lifterlms/lifterlms-helper": "3.4.1", - "lifterlms/lifterlms-rest": "1.0.0-beta.21", - "woocommerce/action-scheduler": "3.4.0" - }, - "require-dev": { - "lifterlms/lifterlms-tests": "^3.1.0", - "lifterlms/lifterlms-cs": "dev-trunk" - }, - "archive": { - "exclude": [ - ".*", - "*.lock", - "*.xml", - "*.xml.dist", - "*.config.js", - - "CHANGELOG.md", - "composer.json", - "docker-compose.yml", - "lerna.json", - "package.json", - "package-lock.json", - "README.md", - - "/assets/scss", - - "_private", - "dist", - "docs", - "gulpfile.js", - "node_modules", - "packages", - "src", - "tests", - "tmp", - "wordpress", - "!/vendor", - - "!/libraries", - "/libraries/README.md", - "/libraries/**/composer.*", - "/libraries/**/i18n", - - "/vendor/bin", - "/vendor/**/**/composer.*", - "/vendor/**/**/*.md", - "/vendor/**/**/.*", - "/vendor/composer/installers", - "/vendor/composer/lifters", - - "!/assets/maps/js/vendor", - "!/assets/vendor", - "!/assets/js/vendor", - "!/assets/js/builder/vendor" - ] - }, - "scripts": { - "check-cs": "\"vendor/bin/phpcs\" --colors", - "check-cs-errors": "\"vendor/bin/phpcs\" --colors --error-severity=1 --warning-severity=6", - "config-cs": [ - "\"vendor/bin/phpcs\" --config-set installed_paths ../../../vendor/wp-coding-standards/wpcs,../../../vendor/lifterlms/lifterlms-cs,../../../vendor/phpcompatibility/php-compatibility,../../../vendor/phpcompatibility/phpcompatibility-paragonie,../../../vendor/phpcompatibility/phpcompatibility-wp", - "\"vendor/bin/phpcs\" --config-set default_standard 'LifterLMS Core'", - "\"vendor/bin/phpcs\" --config-set ignore_warnings_on_exit 1" - ], - "env": "\"vendor/bin/llms-env\"", - "env:setup": "./tests/bin/setup-e2e.sh", - "fix-cs": "\"vendor/bin/phpcbf\"", - "post-install-cmd": "@post-update-install-cmd", - "post-update-cmd": "@post-update-install-cmd", - "post-update-install-cmd": [ - "@config-cs", - "rm -rf ./wp-content/", - "rm composer.lock" - ], - "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}", - "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:-trunk}\"", - "tests-reinstall": [ - "@tests-remove", - "@tests-install" - ], - "tests": "\"vendor/bin/phpunit\"", - "tests-run": "\"vendor/bin/phpunit\"", - "install-php8": "composer install --ignore-platform-reqs" - }, - "extra": { - "installer-paths": { - "libraries/{$name}": [ - "lifterlms/lifterlms-blocks", - "lifterlms/lifterlms-cli", - "lifterlms/lifterlms-helper", - "lifterlms/lifterlms-rest" - ], - "vendor/{$vendor}/{$name}": [ - "type:wordpress-plugin" - ] - } - }, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "composer/installers": true - } - } -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index b773b52d93..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -version: '3.1' -services: - wordpress: - volumes: - - ./:/var/www/html/wp-content/plugins/lifterlms:rw diff --git a/docs/coding-standards.md b/docs/coding-standards.md deleted file mode 100644 index e25d33af7f..0000000000 --- a/docs/coding-standards.md +++ /dev/null @@ -1,141 +0,0 @@ -LifterLMS Coding Standards -========================== - -The purpose of the LifterLMS Coding Standards is to create a baseline for collaboration and review within the open source LifterLMS codebase, project, and community. - -The 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. - -## Naming Conventions - -### camelCase should not be used. - -LifterLMS avoids `camelCase` for class names, class methods, functions, and variables. Words should instead be separated by underscores. - -### Class Names - -Class names should use capitalized words separated by underscores. -LifterLMS core class names should be prefixed with `LLMS_`. - - -```php -class LLMS_Student extends LLMS_Abstract_User_Data { [...] } -class LLMS_Data { [...] } -``` - -LifterLMS add-on class names should be prefixed with `LLMS_` as well as an additional add-on prefix. - -```php -class LLMS_AQ_Question_Types { [...] } -class LLMS_SL_Story extends LLMS_Abstract_Database_Store { [...] } -``` - -### Trait Names - -Trait names should use capitalized words separated by underscores. -LifterLMS core trait names should be prefixed with `LLMS_Trait`. - -```php -trait LLMS_Trait_Singleton { [...] } -``` - -### Constants - -Constants should be in all upper-case with underscores separating words. -LifterLMS core constants should be prefixed with `LLMS_`. - -```php -define( 'LLMS_PLUGIN_FILE', __FILE__ ); -``` - -LifterLMS add-on class names should be prefixed with `LLMS_` as well as an additional add-on prefix. - -```php -define( 'LLMS_FORMIDABLE_FORMS_PLUGIN_FILE', __FILE__ ); -``` - -### File names - -Files should be named descriptively using lower case letters. Hyphens should be used to separate words. - -``` -my-plugin-file.php -``` - -Class 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: - -``` -class-llms-data.php -``` - -Files containing model classes should prepend `model-` instead of `class-`. For example the `LLMS_Student` model class becomes: - -``` -model-llms-student.php -``` - -Trait file names should be based on the trait name with underscores replaced by hyphens and the file stored in the -`includes/traits` directory. For example `LLMS_Trait_Singleton` becomes: - -``` -includes/traits/llms-trait-singleton.php -``` - -### Functions & Variables - -Lowercase letters should be used for function names and variables. Separate words with underscores. -LifterLMS core functions should be prepended with the prefix `llms_`. - -```php -llms_current_time( $type, $gmt = 0 ) { [...] } -``` - -LifterLMS add-on function names should be prefixed with `llms_` as well as an additional add-on prefix. - -```php -llms_ck_consent_form_field() { [...] } -``` - -### Hooks: Actions & Filters - -Lowercase letters should be used for hook names. Separate words with underscores. -LifterLMS core hooks should be prepended with the prefix `llms_`. - -```php -do_action( 'llms_user_enrolled_in_course', [...] ); -apply_filters( 'llms_get_enrollment_status', [...] ); -``` - -LifterLMS add-on hook names should be prefixed with `llms_` as well as an additional add-on prefix. - -```php -do_action( 'llms_pa_post_created_from_automation', [...] ); -apply_filters( 'llms_sl_story_can_user_manage', [...] ); -``` - -When 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. - -There 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. - -### CSS Classes and IDs - -Class names and IDs should be lowercase and prefixed with `llms-`. - -Words should be separated with hyphens (AKA "kebab case"). - -```html -<div class="llms-element-name" id="llms-element-id"></div> -``` - -### Form Element `name` attributes - -The `name` attribute of HTML form elements should be prefixed with `llms_`. - -Lowercase letters should be used and words should be separated by underscores. - -```html -<form> - <input name="llms_text_field" type="text"> -</form> -``` - - diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index aca594f40c..0000000000 --- a/docs/contributing.md +++ /dev/null @@ -1,4 +0,0 @@ -Contributor Guidelines ----------------------- - -See contributing guidelines at https://github.com/gocodebox/lifterlms/blob/trunk/.github/CONTRIBUTING.md diff --git a/docs/documentation-standards.md b/docs/documentation-standards.md deleted file mode 100644 index d9ff0eef6c..0000000000 --- a/docs/documentation-standards.md +++ /dev/null @@ -1,395 +0,0 @@ -LifterLMS Inline Documentation Standards -======================================== - -The 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. - -## What should be documented - -The following elements should be documented using formatted documentation blocks (DocBlocks): - -+ Functions -+ Classes -+ Class methods -+ Class members (including properties and constants) -+ Requires and includes -+ Hooks (actions and filters) -+ File headers - -## DocBlock Formatting Guidelines - -Inline 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. - - -### Spacing - -DocBlocks 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. - - -### Summary - -A 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. - -HTML 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.```. - - -### Description - -An optional longer piece of text providing more details on the associated element’s function. - -HTML may not be used in the summary but markdown can be used to format a complicated description. - -**1. Lists** - -Use a hyphen (`-`) to create an unordered list, with a blank line before and after. - -``` - * Description which includes an unordered list: - * - * - This is item 1. - * - This is item 2. - * - This is item 3. - * - * The description continues on ... -``` - -Use numbers to create an ordered list, with a blank line before and after. - -``` - * Description which includes an ordered list: - * - * 1. This is item 1. - * 2. This is item 2. - * 3. This is item 3. - * - * The description continues on ... -``` - -**2. Code Samples** - -A 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. - -``` - * Description including a code sample: - * - * $status = array( - * 'draft' => __( 'Draft' ), - * 'pending' => __( 'Pending Review' ), - * 'private' => __( 'Private' ), - * 'publish' => __( 'Published' ) - * ); - * - * The description continues on ... -``` - -**3. Links** - -A 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. - -``` - * Description text. - * - * @link https://github.com/gocodebox/lifterlms/issues/1234567890 -``` - -### Changelogs - -Whenever 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. - -No 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`. - -All descriptions for any of these tags should be a full sentence ending with a full stop (a period, for example). - -#### Changes Warranting a Changelog Entry - -Most code changes warrant a changelog entry to be recorded for the element but there are some exceptions. - -+ **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. -+ **Functions and class methods**: Any change made requires an accompanying `@since` tag entry - -Changes 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. - -#### Recording the Version Number - -Versions should be expressed in the 3-digit `x.x.x` style. - -``` - * @since 3.29.0 -``` - -When 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. - -``` - * @since 3.3.0 - * @since 3.5.0 Added optional 3rd argument. -``` - -#### Deprecations - -When an element is marked for deprecation this should be recorded at the end of the changelog with an `@deprecated` tag. - -A 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. - -``` - * @since 3.3.0 - * @since 3.5.0 Added optional 3rd argument. - * @deprecated 3.10.0 Use `llms_new_function_name()` instead. - * - * @see llms_new_function_name() -``` - -When 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. - -#### File Headers - -Whenever an element within a file is updated, the `@version` tag in the header should be updated to the current version of the codebase. - -#### Tag alignment and order - -All 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. - -``` - * @since 3.3.0 - * @since 3.5.0 Changelog entry description. - * @deprecated 3.10.0 Use `llms_new_function_name()` instead. -``` - -When multiple lines are required for a single entry, subsequent lines should be indented to match the starting point of the description. - -``` - * @since 3.3.0 - * @since 3.5.0 Changelog entry description. - A second entry aligned to with the first entry. -``` - -Multiple logs with version numbers of differing lengths should not be aligned to one another. - -``` - * @since 3.3.0 - * @since 3.25.0 Changelog entry description. - * @since 4.0.0 This entry should not be aligned with the 3.25.0 entry above it. -``` - -#### Using Placeholders - -When contributing code we recommend using the placeholder `[version]` in favor of trying to guess what version the element will be released with. - -Our release workflow automatically replaces with `@since`, `@version`, and `@deprecated` followed by `[version]` with the actual version of the release being packaged. - -For a new element: - -``` - * @since [version] -``` - -When updating an existing element: - -``` - * @since 3.5.0 - * @since [version] Updated element. -``` - - -### Additional Tags - -#### 1. Parameters and Returns - -Functions and methods should define all parameter arguments and returns with the `@param` and `@return` tags. - -No 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`. - -All descriptions for any of these tags should be a full sentence ending with a full stop (a period, for example). - -``` - * @param string $var1 Description of the argument. - * @param bool $var2 Description of the argument. - * @return string - */ -function my_function( $var1, $var2 = false ) { - ... - return $var1; -} -``` - -Parameters 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: - -``` - * @type type $key Description. Default 'value'. Accepts 'value', 'value'. - * (aligned with Description, if wraps to a new line) -``` - -A full array parameter would look like this: - -``` - * @param array $args { - * Optional. An array of arguments. - * - * @type type $key Description. Default 'value'. Accepts 'value', 'value'. - * (aligned with Description, if wraps to a new line) - * @type type $key Description. - * } -``` - -#### 2. Types - -Variables, constants, and class members should use the `@var` tag to describe the member's type. - -``` - * @var string - */ -public $var = 'text'; -``` - -#### 3. Relations and References - -Use `@see` to perform automatic links to other areas of the codebase. For example `{@see 'is_lifterlms'}` to link to the filter `is_lifterlms`. - - -#### 4. Thrown Exceptions - -A function or method which throws an exception should document the thrown exception using an `@throws` tag. - -When 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. - -``` - * @return string - * - * @throws Exception A description of the raised exception. - */ -``` - -## DocBlock Examples - - -### Functions and Class Methods - -Functions and class methods should be formatted as follows: - -+ Summary -+ Description (optional) -+ Changelog -+ Links and References (where appropriate) -+ Parameters -+ Return - -``` -/** - * Summary. - * - * Description. - * - * @since x.x.x - * @since x.x.x Description of function/method changes. - * - * @see Function/method/class relied on - * @link URL - * - * @param type $var Description. - * @param type $var Optional. Description. Default. - * @return type Description. - */ -``` - - -### Classes - -Class DocBlocks should be formatted as follows: - -+ Summary -+ Description (Optional) -+ Links and References (as an example use `@see` to reference a super class when documenting a sub class) -+ Changelog - -``` -/** - * Summary. - * - * Description. - * - * @see Super_Class - * - * @since x.x.x - * @since x.x.x Description of class changes. - */ -``` - - -### Class Members - -Class properties and constants should be formatted as follows: - -+ Summary -+ Changelog -+ Type - -``` -/** - * Summary. - * - * @since x.x.x - * @since x.x.x Description of member changes. - * @var type Optional description. - */ -``` - - -### Hooks (Actions and Filters) - -Both 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: - -+ Summary -+ Description (Optional) -+ Changelog -+ Parameters - -Note that `@return` is not used for hook documentation, because action hooks return nothing, and filter hooks always return their first parameter. - -``` -/** - * Summary. - * - * Description. - * - * @since x.x.x - * @since x.x.x Description of hook changes. - * - * @param type $var Description. - * @param array $args { - * Short description about this hash. - * - * @type type $var Description. - * @type type $var Description. - * } - * @param type $var Description. - */ -``` - - -### File Headers - -The file header DocBlock is used to give an overview of what is contained in the file and should be formatted as follows: - -+ Summary -+ Description (optional) -+ Links and references -+ Package -+ Changelog - -``` -/** - * Summary (no period for file headers) - * - * Description. (use period) - * - * @link URL - * - * @package LifterLMS/SecondaryPackage/TertiaryPackage - * - * @since x.x.x - * @since x.x.x Description of file changes. - * @version x.x.x - */ -``` - - -[llms-dev]: https://developer.lifterlms.com/reference/ -[wp-core-docs]: https://developer.wordpress.org/coding-standards/inline-documentation-standards/ diff --git a/docs/e2e-tests-real.md b/docs/e2e-tests-real.md deleted file mode 100644 index d4b4c2ba65..0000000000 --- a/docs/e2e-tests-real.md +++ /dev/null @@ -1,72 +0,0 @@ -Running E2E (End to End) Tests Against a Real Website -===================================================== - -_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._ - -_To run tests locally against managed Docker containers, see the [E2E Testing README](../tests/e2e/README.md)._ - -**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.** - -**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!** - -## Prerequisites - -+ Ability to use a terminal -+ git -+ node.js -+ npm - - -## Setup your local environment - -+ Install the LifterLMS repo: `git clone https://github.com/gocodebox/lifterlms` -+ Move into the cloned directory: `cd liferlms` -+ Install node packages: `npm ci` -+ Create a new file in the created directory named `.llmsenv`. -+ 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): - -``` -WP_BASE_URL=https://yourwebsiteurl.tld -WP_USERNAME=adminusername -WP_PASSWORD=adminpassword -``` - -**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.** - -+ Save the file - - -## Setup your production site - -+ Install and activate the LifterLMS plugin on your site - - -## Run the tests - -There are two ways to run the E2E tests: - -### Headless mode - -Runs the tests and shows you the results. - -If 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. - -Error logs will be output in your terminal to review. - -Run headless tests by executing `npm run tests` in your terminal. - - -### Interactive mode - -Launches an automated Chromium browser and runs the tests in "slow motion" so you can watch as the tests run. - -No screenshots are takeng in interactive mode. - -Error logs are output to the terminal for review. - -Run interactive tests by executing `npm run tests:dev` in your terminal. - - -### Using environment variables - -If 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` diff --git a/docs/installing.md b/docs/installing.md deleted file mode 100644 index 6bb012f3c3..0000000000 --- a/docs/installing.md +++ /dev/null @@ -1,80 +0,0 @@ -Installing for Development -========================== - -## Requirements - -In order to build and develop LifterLMS locally, you'll need the following: - -+ PHP -+ MySQL / MariaDB -+ [Composer](https://getcomposer.org/download/) -+ [Node.js](https://nodejs.org/en/download/) -+ [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - - -## Building LifterLMS - -### 1. Clone source from GitHub - -```sh -$ git clone https://github.com/gocodebox/lifterlms -$ cd lifterlms -``` - -If you're planning to contribute code, you should fork this repository and clone your fork instead. - - -### 2. Install composer dependencies: - -```sh -$ composer install -``` - -### 3. Install npm dependencies: - -```sh -$ npm install --global gulp -$ npm install -``` - -### 4. Build static assets - -```sh -$ gulp build -``` - -The `lifterlms` directory is now an installable plugin that can be moved into your local server's `wp-content/plugins` directory. - - -## Running PHPCS - -When contributing you should ensure your contributions follow our [coding](./coding-standards.md) and [documentation](./documentation-standards.md) standards. - -To check for errors and warnings in your code, run PHPCS: - -```sh -$ composer run check-cs -``` - -To check for errors only: - -```sh -$ composer run check-cs-errors -``` - -These reports may include issues that can be automatically fixed using PHPCBF: - -```sh -$ composer run fix-cs -``` - -## Running Test Suites - -New code should also strive to be covered by automated tests. - -LifterLMS has unit and integration tests via phpunit and End-to-End tests via Jest and Puppeteer. - -For guides on running and contributing tests, see the relevant guides: - -+ [phpunit](../tests/phpunit/README.md) -+ [e2e](../tests/e2e/README.md) diff --git a/docs/releases.md b/docs/releases.md deleted file mode 100644 index 6520df1f77..0000000000 --- a/docs/releases.md +++ /dev/null @@ -1,62 +0,0 @@ -Releasing LifterLMS Builds -========================== - -This document outlines the workflow used by LifterLMS core maintainers to build and publish LifterLMS releases. - -This document assumes you have already installed LifterLMS for development following the [Installing for Development guide](./installing.md). - -## 1. Build the Release - -Prepare the release: `npm run dev release prepare`: - -When running this command, the following happens: - -1. Determines the version number based on the significance values found in `.changelogs/` files. Unless `-F` is passed to the command to force a specific version number. -2. Write the changelog entries to `CHANGELOG.md`. -3. Updates version numbers of placeholder `[version]` tags, `package.json`, etc... -4. Runs the release build command, `npm run build`. - -## 2. Run tests and coding standards checks - -1. Ensure phpunit tests pass: `composer run tests-run`. -2. Ensure phpcs checks pass: `composer run check-cs-errors`. -3. Ensure e2e tests pass: `npm run test`. -4. Ensure eslint checks pass: `npm run lint:js`. - -## 3. Commit and push - -After building and testing the built release, all changes should be committed and pushed to GitHub. - -## 3. Generate the Distribution Archive - -Run `npm run dev release archive`. - -## 4. Run pre-release tests on the archived - -Install and activate the zip file on a temporary sandbox site. - - 1. Run the setup wizard. - 2. Import sample course - 3. Enroll a student into the course. - 4. Complete a lesson. - -_This manual testing ensures no errors occurred in the build steps above._ - -## 5. Publish the Release - -Run `npm run dev release create`. - -The following steps are performed automatically by the above task: - -1. Publish to GitHub - A. The contents of the distribution archive is force-pushed to the `release` branch. - B. A new release tag draft is created for the current version number using `release` as the commit target. - C. The distribution archive is uploaded to the release. - D. The release is published. - E. A webhook ping notifies the `llms-releaser` server which performs the remaining steps of the release: -2. Publish to WordPress plugin repository - A. Create a new SVN tag using the release asset (distribution archive) as the base. - B. Update the `trunk` branch to match the new tag. -3. A changelog blog post is published to make.lifterlms.com. -4. The number is updated at LifterLMS.com -5. The distribution archive is synced to the release asset bucket in AWS S3 as a backup. diff --git a/gulpfile.js/index.js b/gulpfile.js/index.js deleted file mode 100644 index 6b6010bbd7..0000000000 --- a/gulpfile.js/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Main Gulp File - * - * Requires all task files - */ -var gulp = require('gulp'); - -// All custom tasks. -require( './tasks/js-additional' ); -require( './tasks/js-builder' ); - -// All tasks from lib-tasks. -require( 'lifterlms-lib-tasks' )( gulp ); diff --git a/gulpfile.js/tasks/js-additional.js b/gulpfile.js/tasks/js-additional.js deleted file mode 100644 index 6e13545bab..0000000000 --- a/gulpfile.js/tasks/js-additional.js +++ /dev/null @@ -1,49 +0,0 @@ -var gulp = require( 'gulp' ) - , header = require( 'gulp-header' ) - , include = require( 'gulp-include' ) - , maps = require( 'gulp-sourcemaps' ) - , pump = require( 'pump' ) - , rename = require( 'gulp-rename' ) - , uglify = require( 'gulp-uglify' ) - , gulpignore = require( 'gulp-ignore' ) - - , path = require( 'path' ) -; - -gulp.task( 'js-additional', function( cb ) { - - var notice = [ - '/****************************************************************', - ' *', - ' * Contributor\'s Notice', - ' * ', - ' * This is a compiled file and should not be edited directly!', - ' * The uncompiled script is located in the "assets/private" directory', - ' * ', - ' ****************************************************************/', - '', - '', - ]; - - pump( [ - gulp.src( 'assets/js/private/**/*.js' ), - include(), - maps.init(), - header( notice.join( '\n' ) ), - maps.write('../maps/js', { destPath: 'assets/js' } ), - gulp.dest( 'assets/js' ), - - // Don't pass maps any further. - gulpignore.exclude( file => '.js' !== path.extname( file.basename ) ), - - uglify(), - rename( { - suffix: '.min', - } ), - maps.write('../maps/js', { destPath: 'assets/js' } ), - gulp.dest( 'assets/js' ) - ], - cb - ); - -} ); diff --git a/gulpfile.js/tasks/js-builder.js b/gulpfile.js/tasks/js-builder.js deleted file mode 100644 index 869fa7758e..0000000000 --- a/gulpfile.js/tasks/js-builder.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * ----------------------------------------------------------- - * js-builder - * ----------------------------------------------------------- - * Compile Admin builder Javascript - */ - -var gulp = require( 'gulp' ) - , notify = require( 'gulp-notify' ) - , requirejsOptimize = require( 'gulp-requirejs-optimize' ) - , rename = require( 'gulp-rename' ) - , sourcemaps = require( 'gulp-sourcemaps' ) -; - -gulp.task( 'js-builder', function( cb ) { - - gulp.src( 'assets/js/builder/main.js' ) - // unminified - .pipe( sourcemaps.init() ) - .pipe( requirejsOptimize( function( file ) { - return { - name: 'vendor/almond', - optimize: 'none', - wrap: { - start: "(function($){", - end: "}(jQuery));" - }, - baseUrl: 'assets/js/builder/', - include: [ 'main' ], - preserveLicenseComments: false - }; - } ).on( 'error', notify.onError( { - message: '<%= error.message %>', - sound: 'Frog', - title: 'js-builder error' - } ) ) ) - .pipe( rename( 'llms-builder.js' ) ) - .pipe( sourcemaps.write( '../maps/js', { destPath: 'assets/js' } ) ) - .pipe( gulp.dest( 'assets/js/' ) ) - - // minified - .pipe( sourcemaps.init() ) - .pipe( requirejsOptimize( function( file ) { - return { - name: 'vendor/almond', - optimize: 'uglify2', - wrap: { - start: "(function($){", - end: "}(jQuery));" - }, - baseUrl: 'assets/js/builder/', - include: [ 'main' ], - preserveLicenseComments: false - }; - } ).on( 'error', notify.onError( { - message: '<%= error.message %>', - sound: 'Frog', - title: 'js-builder error' - } ) ) ) - .pipe( rename( 'llms-builder.min.js' ) ) - .pipe( sourcemaps.write( '../maps/js', { destPath: 'assets/js' } ) ) - .pipe( gulp.dest( 'assets/js/' ) ); - - cb(); - -}); diff --git a/includes/abstracts/abstract.llms.post.model.php b/includes/abstracts/abstract.llms.post.model.php old mode 100755 new mode 100644 diff --git a/includes/admin/reporting/tables/llms.table.course.students.php b/includes/admin/reporting/tables/llms.table.course.students.php index a6c7b29989..9141a99d46 100644 --- a/includes/admin/reporting/tables/llms.table.course.students.php +++ b/includes/admin/reporting/tables/llms.table.course.students.php @@ -5,7 +5,7 @@ * @package LifterLMS/Admin/Reporting/Tables/Classes * * @since 3.2.0 - * @version 3.18.0 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -267,12 +267,13 @@ public function get_table_search_form_placeholder() { } /** - * Execute a query to retrieve results from the table + * Execute a query to retrieve results from the table. * - * @param array $args array of query args - * @return void - * @since 3.15.0 - * @version 3.15.0 + * @since 3.15.0 + * @since 5.10.0 Add ability to sort by completion date. + * + * @param array $args Array of query args. + * @return void */ public function get_results( $args = array() ) { @@ -299,6 +300,15 @@ public function get_results( $args = array() ) { $sort = array(); switch ( $this->get_orderby() ) { + case 'completed': + $sort = array( + 'completed' => $this->get_order(), + 'last_name' => 'ASC', + 'first_name' => 'ASC', + 'id' => 'ASC', + ); + break; + case 'enrolled': $sort = array( 'date' => $this->get_order(), diff --git a/includes/class-llms-block-templates.php b/includes/class-llms-block-templates.php index fa05f0c4b2..3ece38a320 100644 --- a/includes/class-llms-block-templates.php +++ b/includes/class-llms-block-templates.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 5.8.0 - * @version 5.9.0 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -474,6 +474,7 @@ private function get_maybe_overridden_block_template_file_path( $template_file ) * * @since 5.8.0 * @since 5.9.0 Return empty string if the passed path is not in the configuration. + * @since 5.10.0 Use '/' in favor of DIRECTORY_SEPARATOR to avoid issues on Windows. * * @param string $path The template's path. * @return string @@ -486,7 +487,7 @@ private function generate_template_slug_from_path( $path ) { return $dirname ? $prefix . substr( $path, - strpos( $path, $dirname . DIRECTORY_SEPARATOR ) + 1 + strlen( $dirname ), + strpos( $path, $dirname . '/' ) + 1 + strlen( $dirname ), -5 // .html ) : diff --git a/includes/class.llms.course.data.php b/includes/class.llms.course.data.php index e6c90879ab..82aa523c6b 100644 --- a/includes/class.llms.course.data.php +++ b/includes/class.llms.course.data.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 3.15.0 - * @version 4.21.0 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -151,9 +151,10 @@ public function get_engagements( $type, $period = 'current' ) { } /** - * Retrieve # of lessons completed within the period + * Retrieves and returns the number of lessons completed within the period. * * @since 3.15.0 + * @since 5.10.0 Fixed issue when the course has no lessons. * * @param string $period Optional. Date period [current|previous]. Default is 'current'. * @return int @@ -164,6 +165,12 @@ public function get_lesson_completions( $period = 'current' ) { $lessons = implode( ',', $this->post->get_lessons( 'ids' ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + // Return early for courses without any lessons. + if ( empty( $lessons ) ) { + return 0; + } + return $wpdb->get_var( $wpdb->prepare( " diff --git a/includes/class.llms.student.query.php b/includes/class.llms.student.query.php index 04c3791eed..6d2f3bfe94 100644 --- a/includes/class.llms.student.query.php +++ b/includes/class.llms.student.query.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 3.4.0 - * @version 4.10.2 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -337,11 +337,12 @@ private function sql_search() { } /** - * Set up the SQL for the select statement + * Set up the SQL for the select statement. * * @since 3.13.0 * @since 4.10.2 Drop usage of `this->get_filter( 'select' )` in favor of `'llms_student_query_select'`. * Use `$this->sql_select_columns({columns})` to determine additional columns to select. + * @since 5.10.0 Add a subquery for completed date. * * @return string */ @@ -357,6 +358,7 @@ private function sql_select() { // All the possible fields. $fields = array( + 'completed' => "( {$this->sql_subquery( 'updated_date', '_is_complete' )} ) AS completed", 'date' => "( {$this->sql_subquery( 'updated_date' )} ) AS `date`", 'last_name' => 'm_last.meta_value AS last_name', 'first_name' => 'm_first.meta_value AS first_name', @@ -417,16 +419,18 @@ private function sql_status_in( $column = 'status' ) { } /** - * Generate an SQL subquery for the dynamic status or date values in the main query + * Generate an SQL subquery for the meta key in the main query. * * @since 3.13.0 + * @since 5.10.0 Add `$meta_key` argument. * - * @param string $column Column name. + * @param string $column Column name. + * @param string $meta_key Optional meta key to use in the WHERE condition. Defaults to '_status'. * @return string */ - private function sql_subquery( $column ) { + private function sql_subquery( $column, $meta_key = '_status' ) { - $and = ''; + global $wpdb; $post_ids = $this->get( 'post_id' ); if ( $post_ids ) { @@ -436,11 +440,9 @@ private function sql_subquery( $column ) { $and = "AND {$this->sql_status_in( 'meta_value' )}"; } - global $wpdb; - return "SELECT {$column} FROM {$wpdb->prefix}lifterlms_user_postmeta - WHERE meta_key = '_status' + WHERE meta_key = '{$meta_key}' AND user_id = id {$and} ORDER BY updated_date DESC diff --git a/includes/forms/class-llms-form-field.php b/includes/forms/class-llms-form-field.php index aba971d5a2..34cf798d31 100644 --- a/includes/forms/class-llms-form-field.php +++ b/includes/forms/class-llms-form-field.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 5.0.0 - * @version 5.9.0 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -755,6 +755,7 @@ protected function prepare_options_from_preset() { * Additional preparation for the password strength meter. * * @since 5.0.0 + * @since 5.10.0 Make sure to enqueue the strength meter js, whether or not `wp_enqueue_scripts` hook has been fired yet. * * @return void */ @@ -770,7 +771,7 @@ protected function prepare_password_strength_meter() { unset( $this->settings['min_length'] ); /** - * Modify password strength meter settings + * Modify password strength meter settings. * * @since 5.0.0 * @@ -784,18 +785,44 @@ protected function prepare_password_strength_meter() { */ $meter_settings = apply_filters( 'llms_password_strength_meter_settings', $meter_settings, $this->settings, $this ); - // If scripts have been enqueued, add password strength meter script and localize with meter data. + // If scripts have been enqueued, add password strength meter script. if ( did_action( 'wp_enqueue_scripts' ) ) { + return $this->enqueue_strength_meter( $meter_settings ); + } + // Otherwise add it whe `wp_enqueue_scripts` is fired. + add_action( + 'wp_enqueue_scripts', + function() use ( $meter_settings ) { + $this->enqueue_strength_meter( $meter_settings ); + } + ); - wp_enqueue_script( 'password-strength-meter' ); - llms()->assets->enqueue_inline( - 'llms-pw-strength-settings', - 'window.LLMS.PasswordStrength = window.LLMS.PasswordStrength || {};window.LLMS.PasswordStrength.get_settings = function() { return JSON.parse( \'' . wp_json_encode( $meter_settings ) . '\' ); };', - 'footer', - 15 - ); + } - } + /** + * Enqueue password strength meter script. + * + * @since 5.10.0 + * + * @param array $meter_settings { + * Hash of meter configuration options. + * + * @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. + * @type string $min_strength The minimum acceptable password strength. Accepts "strong", "medium", or "weak". Default: "strong". + * @type int $min_length The minimum acceptable password length. Must be >= 6. Default: 6. + * } + * @return void + */ + private function enqueue_strength_meter( $meter_settings ) { + + wp_enqueue_script( 'password-strength-meter' ); + // Localize the script with meter data. + llms()->assets->enqueue_inline( + 'llms-pw-strength-settings', + 'window.LLMS.PasswordStrength = window.LLMS.PasswordStrength || {};window.LLMS.PasswordStrength.get_settings = function() { return JSON.parse( \'' . wp_json_encode( $meter_settings ) . '\' ); };', + 'footer', + 15 + ); } diff --git a/includes/forms/class-llms-form-post-type.php b/includes/forms/class-llms-form-post-type.php index 9dbd347375..99bcf7c24b 100644 --- a/includes/forms/class-llms-form-post-type.php +++ b/includes/forms/class-llms-form-post-type.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 5.0.0 - * @version 5.0.0 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -276,19 +276,25 @@ public function register_post_type() { * Register custom postmeta properties for the forms post type. * * @since 5.0.0 + * @since 5.10.0 Added new meta for checkout forms and free access plans. * * @return void */ public function register_meta() { $props = array( - '_llms_form_location' => array( + '_llms_form_location' => array( 'description' => __( 'Determines the front-end location where the form is displayed.', 'lifterlms' ), ), - '_llms_form_show_title' => array( + '_llms_form_show_title' => array( 'description' => __( 'Determines whether or not to display the form\'s title on the front-end.', 'lifterlms' ), ), - '_llms_form_is_core' => array( + // This is only actually used for 'checkout' forms. + '_llms_form_title_free_access_plans' => array( + 'description' => __( 'The alternative form title to be shown on checkout for free access plans.', 'lifterlms' ), + 'default' => __( 'Student Information', 'lifterlms' ), + ), + '_llms_form_is_core' => array( 'description' => __( 'Determines if the form is a core form required for basic site functionality.', 'lifterlms' ), ), ); diff --git a/includes/functions/llms-functions-forms.php b/includes/functions/llms-functions-forms.php index 134b134648..b28c1cae83 100644 --- a/includes/functions/llms-functions-forms.php +++ b/includes/functions/llms-functions-forms.php @@ -5,7 +5,7 @@ * @package LifterLMS/Functions/Forms * * @since 5.0.0 - * @version 5.0.1 + * @version 5.10.0 */ defined( 'ABSPATH' ) || exit; @@ -75,6 +75,7 @@ function llms_get_form_html( $location, $args = array() ) { * Returns an empty string if the form is disabled via form settings. * * @since 5.0.0 + * @since 5.10.0 Return specific form title for checkout forms and free access plans. * * @param string $location Form location, one of: "checkout", "registration", or "account". * @param array $args Additional arguments passed to the short-circuit filter in `LLMS_Forms->get_form_post()`. @@ -87,7 +88,11 @@ function llms_get_form_title( $location, $args = array() ) { return ''; } - return get_the_title( $post->ID ); + return 'checkout' === $location && isset( $args['plan'] ) && $args['plan']->is_free() + ? + apply_filters( 'the_title', get_post_meta( $post->ID, '_llms_form_title_free_access_plans', true ) ) + : + get_the_title( $post->ID ); } diff --git a/languages/README.md b/languages/README.md deleted file mode 100644 index 4858d32b7a..0000000000 --- a/languages/README.md +++ /dev/null @@ -1,24 +0,0 @@ -LifterLMS Localization and Language Files -========================================= - -This directory contains localization and language files for the LifterLMS plugin. - -## Translating LifterLMS - -LifterLMS 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. - - -## Localization Information Files - -The `.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. - -The 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. - -Information for these files is derived from the following projects and sources: - -+ [Countries States Cities Database](https://github.com/dr5hn/countries-states-cities-database) -+ [Currency Formatter](https://github.com/smirzaei/currency-formatter) -+ [addressfield.json](https://github.com/tableau-mkt/addressfield.json) -+ [LocalePlanet](https://www.localeplanet.com/) - -If 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). diff --git a/languages/lifterlms.pot b/languages/lifterlms.pot index 7bde49d7fb..c8077e92cd 100644 --- a/languages/lifterlms.pot +++ b/languages/lifterlms.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv3. msgid "" msgstr "" -"Project-Id-Version: LifterLMS 5.9.0\n" +"Project-Id-Version: LifterLMS 5.10.0\n" "Report-Msgid-Bugs-To: https://lifterlms.com/my-account/my-tickets\n" "Last-Translator: Team LifterLMS <team@lifterlms.com>\n" "Language-Team: Team LifterLMS <team@lifterlms.com>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2022-02-15T13:32:50-07:00\n" +"POT-Creation-Date: 2022-02-22T12:01:16-07:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: llms/dev 0.0.4-alpha.0\n" "X-Domain: lifterlms\n" @@ -283,7 +283,7 @@ msgid "Basic" msgstr "" #: includes/abstracts/llms.abstract.notification.controller.php:599 -#: includes/admin/reporting/tables/llms.table.course.students.php:416 +#: includes/admin/reporting/tables/llms.table.course.students.php:426 #: includes/admin/reporting/tables/llms.table.membership.students.php:404 #: includes/admin/reporting/tables/llms.table.students.php:597 #: includes/class.llms.post-types.php:832 @@ -362,7 +362,7 @@ msgstr "" #: includes/abstracts/llms.abstract.notification.view.quiz.completion.php:57 #: includes/abstracts/llms.abstract.notification.view.quiz.completion.php:93 #: includes/admin/post-types/tables/class.llms.table.student.management.php:401 -#: includes/admin/reporting/tables/llms.table.course.students.php:442 +#: includes/admin/reporting/tables/llms.table.course.students.php:452 #: includes/admin/reporting/tables/llms.table.quiz.attempts.php:284 #: includes/admin/reporting/tables/llms.table.student.course.php:269 #: includes/admin/reporting/tables/llms.table.student.courses.php:238 @@ -382,7 +382,7 @@ msgstr "" #: includes/abstracts/llms.abstract.notification.view.quiz.completion.php:58 #: includes/admin/class.llms.admin.menus.php:208 #: includes/admin/post-types/tables/class.llms.table.student.management.php:389 -#: includes/admin/reporting/tables/llms.table.course.students.php:422 +#: includes/admin/reporting/tables/llms.table.course.students.php:432 #: includes/admin/reporting/tables/llms.table.membership.students.php:410 #: includes/admin/reporting/tables/llms.table.student.courses.php:235 #: includes/admin/reporting/tables/llms.table.student.memberships.php:134 @@ -2643,7 +2643,7 @@ msgstr "" #: includes/admin/post-types/tables/class.llms.table.student.management.php:380 #: includes/admin/reporting/tables/llms.table.achievements.php:175 #: includes/admin/reporting/tables/llms.table.certificates.php:179 -#: includes/admin/reporting/tables/llms.table.course.students.php:397 +#: includes/admin/reporting/tables/llms.table.course.students.php:407 #: includes/admin/reporting/tables/llms.table.courses.php:300 #: includes/admin/reporting/tables/llms.table.membership.students.php:385 #: includes/admin/reporting/tables/llms.table.memberships.php:300 @@ -2660,7 +2660,7 @@ msgid "ID" msgstr "" #: includes/admin/post-types/tables/class.llms.table.student.management.php:384 -#: includes/admin/reporting/tables/llms.table.course.students.php:401 +#: includes/admin/reporting/tables/llms.table.course.students.php:411 #: includes/admin/reporting/tables/llms.table.membership.students.php:389 #: includes/admin/reporting/tables/llms.table.student.course.php:263 #: includes/admin/reporting/tables/llms.table.student.courses.php:231 @@ -2674,13 +2674,13 @@ msgid "Name" msgstr "" #: includes/admin/post-types/tables/class.llms.table.student.management.php:393 -#: includes/admin/reporting/tables/llms.table.course.students.php:427 +#: includes/admin/reporting/tables/llms.table.course.students.php:437 #: includes/admin/reporting/tables/llms.table.membership.students.php:415 msgid "Enrollment Updated" msgstr "" #: includes/admin/post-types/tables/class.llms.table.student.management.php:397 -#: includes/admin/reporting/tables/llms.table.course.students.php:437 +#: includes/admin/reporting/tables/llms.table.course.students.php:447 #: includes/admin/reporting/tables/llms.table.student.courses.php:241 #: includes/admin/reporting/tables/llms.table.students.php:626 #: includes/privacy/class-llms-privacy-exporters.php:218 @@ -2691,7 +2691,7 @@ msgid "Progress" msgstr "" #: includes/admin/post-types/tables/class.llms.table.student.management.php:405 -#: includes/admin/reporting/tables/llms.table.course.students.php:446 +#: includes/admin/reporting/tables/llms.table.course.students.php:456 msgid "Last Lesson" msgstr "" @@ -2742,7 +2742,7 @@ msgid "All Time" msgstr "" #: includes/admin/reporting/class.llms.admin.reporting.php:297 -#: includes/admin/reporting/tables/llms.table.course.students.php:279 +#: includes/admin/reporting/tables/llms.table.course.students.php:280 #: includes/admin/reporting/tables/llms.table.courses.php:315 #: includes/admin/reporting/tables/llms.table.membership.students.php:267 #: includes/admin/reporting/tables/llms.table.memberships.php:315 @@ -2803,7 +2803,7 @@ msgstr "" msgid "This student has not yet earned any certificates." msgstr "" -#: includes/admin/reporting/tables/llms.table.course.students.php:406 +#: includes/admin/reporting/tables/llms.table.course.students.php:416 #: includes/admin/reporting/tables/llms.table.membership.students.php:394 #: includes/admin/reporting/tables/llms.table.students.php:606 #: includes/schemas/llms-user-information-fields.php:76 @@ -2811,7 +2811,7 @@ msgstr "" msgid "Last Name" msgstr "" -#: includes/admin/reporting/tables/llms.table.course.students.php:411 +#: includes/admin/reporting/tables/llms.table.course.students.php:421 #: includes/admin/reporting/tables/llms.table.membership.students.php:399 #: includes/admin/reporting/tables/llms.table.students.php:611 #: includes/schemas/llms-user-information-fields.php:68 @@ -2819,7 +2819,7 @@ msgstr "" msgid "First Name" msgstr "" -#: includes/admin/reporting/tables/llms.table.course.students.php:432 +#: includes/admin/reporting/tables/llms.table.course.students.php:442 #: includes/admin/reporting/tables/llms.table.student.course.php:272 #: includes/admin/reporting/tables/llms.table.student.courses.php:248 msgid "Completed" @@ -3579,7 +3579,7 @@ msgstr "" #: includes/admin/settings/class.llms.settings.courses.php:100 #: includes/admin/views/setup-wizard/step-pages.php:19 -#: includes/class-llms-block-templates.php:643 +#: includes/class-llms-block-templates.php:644 #: includes/class.llms.install.php:255 msgid "Course Catalog" msgstr "" @@ -4944,7 +4944,7 @@ msgid "This page is where your visitors will find a list of all your available c msgstr "" #: includes/admin/views/setup-wizard/step-pages.php:23 -#: includes/class-llms-block-templates.php:644 +#: includes/class-llms-block-templates.php:645 #: includes/class.llms.install.php:261 msgid "Membership Catalog" msgstr "" @@ -4994,72 +4994,72 @@ msgstr "" msgid "No theme is defined for this template." msgstr "" -#: includes/class-llms-block-templates.php:645 +#: includes/class-llms-block-templates.php:646 msgid "Single Certificate" msgstr "" -#: includes/class-llms-block-templates.php:646 +#: includes/class-llms-block-templates.php:647 msgid "Single Access Restricted" msgstr "" -#: includes/class-llms-block-templates.php:647 +#: includes/class-llms-block-templates.php:648 msgid "Taxonomy Course Category" msgstr "" -#: includes/class-llms-block-templates.php:648 +#: includes/class-llms-block-templates.php:649 msgid "Taxonomy Course Difficulty" msgstr "" -#: includes/class-llms-block-templates.php:649 +#: includes/class-llms-block-templates.php:650 msgid "Taxonomy Course Tag" msgstr "" -#: includes/class-llms-block-templates.php:650 +#: includes/class-llms-block-templates.php:651 msgid "Taxonomy Course Track" msgstr "" -#: includes/class-llms-block-templates.php:651 +#: includes/class-llms-block-templates.php:652 msgid "Taxonomy Membership Category" msgstr "" -#: includes/class-llms-block-templates.php:652 +#: includes/class-llms-block-templates.php:653 msgid "Taxonomy Membership Tag" msgstr "" -#: includes/class-llms-block-templates.php:681 +#: includes/class-llms-block-templates.php:682 msgid "LifterLMS Course Catalog Template" msgstr "" -#: includes/class-llms-block-templates.php:682 +#: includes/class-llms-block-templates.php:683 msgid "LifterLMS Membership Catalog Template" msgstr "" -#: includes/class-llms-block-templates.php:683 +#: includes/class-llms-block-templates.php:684 msgid "LifterLMS Certificate Template" msgstr "" -#: includes/class-llms-block-templates.php:684 +#: includes/class-llms-block-templates.php:685 msgid "LifterLMS Single Template Access Restricted" msgstr "" -#: includes/class-llms-block-templates.php:685 +#: includes/class-llms-block-templates.php:686 msgid "LifterLMS Course Category Taxonomy Template" msgstr "" -#: includes/class-llms-block-templates.php:686 +#: includes/class-llms-block-templates.php:687 msgid "LifterLMS Course Difficulty Taxonomy Template" msgstr "" -#: includes/class-llms-block-templates.php:687 +#: includes/class-llms-block-templates.php:688 msgid "LifterLMS Course Tag Taxonomy Template" msgstr "" -#: includes/class-llms-block-templates.php:688 +#: includes/class-llms-block-templates.php:689 msgid "LifterLMS Course Track Taxonomy Template" msgstr "" -#: includes/class-llms-block-templates.php:689 #: includes/class-llms-block-templates.php:690 +#: includes/class-llms-block-templates.php:691 msgid "LifterLMS Membership Tag Taxonomy Template" msgstr "" @@ -7575,15 +7575,24 @@ msgstr "" msgid "No Forms found in trash" msgstr "" -#: includes/forms/class-llms-form-post-type.php:286 +#: includes/forms/class-llms-form-post-type.php:287 msgid "Determines the front-end location where the form is displayed." msgstr "" -#: includes/forms/class-llms-form-post-type.php:289 +#: includes/forms/class-llms-form-post-type.php:290 msgid "Determines whether or not to display the form's title on the front-end." msgstr "" -#: includes/forms/class-llms-form-post-type.php:292 +#: includes/forms/class-llms-form-post-type.php:294 +msgid "The alternative form title to be shown on checkout for free access plans." +msgstr "" + +#: includes/forms/class-llms-form-post-type.php:295 +#: templates/admin/reporting/tabs/students/information.php:25 +msgid "Student Information" +msgstr "" + +#: includes/forms/class-llms-form-post-type.php:298 msgid "Determines if the form is a core form required for basic site functionality." msgstr "" @@ -33641,10 +33650,6 @@ msgstr "" msgid "Last Activity Date" msgstr "" -#: templates/admin/reporting/tabs/students/information.php:25 -msgid "Student Information" -msgstr "" - #: templates/admin/reporting/tabs/students/information.php:39 msgid "Registered" msgstr "" @@ -34905,6 +34910,14 @@ msgstr "" msgid "Not displaying form title." msgstr "" +#: libraries/lifterlms-blocks/assets/js/llms-blocks.js:24 +msgid "Free Access Plan Form Title" +msgstr "" + +#: libraries/lifterlms-blocks/assets/js/llms-blocks.js:24 +msgid "The form title to be shown for free access plans." +msgstr "" + #: libraries/lifterlms-blocks/assets/js/llms-blocks.js:24 msgid "Revert to Default" msgstr "" diff --git a/lerna.json b/lerna.json deleted file mode 100644 index a2bb50ba7c..0000000000 --- a/lerna.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "packages": [ - "packages/*" - ], - "version": "independent" -} diff --git a/libraries/README.md b/libraries/README.md deleted file mode 100644 index 44ab13842b..0000000000 --- a/libraries/README.md +++ /dev/null @@ -1,6 +0,0 @@ -External Libraries -================== - -Installation directory for plugin libraries included in the core plugin but developed outside of this repository. - -See [Installing for Development](../docs/installing.md) for installation instructions. diff --git a/libraries/lifterlms-blocks/CHANGELOG.md b/libraries/lifterlms-blocks/CHANGELOG.md new file mode 100644 index 0000000000..1c88011389 --- /dev/null +++ b/libraries/lifterlms-blocks/CHANGELOG.md @@ -0,0 +1,442 @@ +LifterLMS Blocks Changelog +========================== + +v2.3.2 - 2022-02-22 +------------------- + +##### Updates and Enhancements + ++ Added an option to specify a custom checkout form title for free access plans. + + +v2.3.1 - 2022-01-26 +------------------- + +##### Updates and Enhancements + ++ Resolved PHP 8.1 deprecation warnings. + + +v2.3.0 - 2022-01-25 +------------------- + +##### New Features + ++ Added the llms/php-template block, used by the Site Editor to load php templates. + +##### Updates and Enhancements + ++ Adds support for WordPress 5.9. ++ The minimum required WordPress version is now 5.5. + + +v2.2.1 - 2021-09-29 +------------------- + ++ Bugfix: Fixed deprecated filter warning encountered when using certain development versions of the WordPress core. + + +v2.2.0 - 2021-07-19 +------------------- + +##### Updates + ++ **Increases minimum WordPress Core version requirement to version 5.4!**. ++ Tested and compatible with WordPress core 5.8 ++ Don't load block editor assets on the "blockified" widgets screen. ++ Remove timeouts and subscription debouncing used by blocks watcher which handles the `llms/user-info-fields` redux store. ++ Stop debouncing the blocks watcher. + +##### Bug fixes + ++ Confirm group blocks now configure the block's id, name, and match attributes instead of being configured in the block render via the `blocks/form-fields/group-data` module. ++ Don't define the `match` attribute during creation of a user password block. + + +v2.1.1 - 2021-07-08 +------------------- + ++ Fixed issue causing visibility controls to display for blocks which have no visibility attributes defined. + + +v2.1.0 - 2021-06-28 +------------------- + +##### Updates + ++ Adjusted priority of block editor JS assets to load at priority `5` instead of `999`. Resolves plugin conflicts encountered when using block-level visibility on blocks registered after visibility filters are applied. ++ Removed usage of [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) and replaced with [dndkit](https://github.com/clauderic/dnd-kit) for drag and drop UX within the editor. ++ Refactored the instructors sidebar (on courses and memberships) as well as the option shorting (for fields with options) to utilize `dndkit`. + +##### Bugfixes + ++ Fixed an issue encountered on password confirmation fields when adjusting the minimum password length option on the user password block. + + +v2.0.1 - 2021-06-21 +------------------- + ++ Use non-unique error notice IDs for reusable multiple error notice. + + +v2.0.0 - 2021-06-21 +------------------- + +##### Updates + ++ Adds LifterLMS User Information form building via the block editor. ++ Initially compatibility for WordPress 5.8 (full site editing). Ensures core functionality but doesn't add any exciting features. ++ Improve the visual feedback inside the editor for a block with visibility restrictions. ++ Added reusable block support for form fields. ++ Adds a user information (`[llms-user]`) shortcode inserter to rich text block toolbars. ++ Use rich text `allowedFormats` in favor of deprecated `formattingControls` ++ Improved localization of Javascript files. + +##### Bug Fixes + ++ Fixed issue encountered when using lesson progression blocks outside of a lesson, thanks [@reedhewitt](https://github.com/reedhewitt)! ++ Fixed fatal errors encountered if LifterLMS core isn't active when this plugin is activated. ++ Currently selected instructors are excluded from queries for instructor users. ++ Fixed issue encountered on courses and memberships when attempting to edit instructor information. + +##### Backwards Incompatible Changes + ++ Major refactor of all field-related blocks. ++ The names of many field blocks have changed. ++ Use `getDisallowedBlocks()` in favor of removed `getBlacklist()` in `block-visibility/check`. ++ Blocks restricted to specific posts have had the post object stored on the block attribute reduced to include only the minimum required properties. ++ The `Search`, `SearchPost`, and `SearchUser` components have had major changes to make them more extendable. ++ Don't render InspectorControls since the block doesn't have any actual settings. + + +v2.0.0-rc.2 - 2021-06-18 +------------------------ + ++ Only load the plugin if LifterLMS is loaded ++ Update version checking method. ++ Fixed typo causing errors on WP 5.6 and earlier. ++ Fix WP 5.7 compatibility issues ++ Fixed issue encountered when using lesson progression blocks outside of a lesson, thanks [@reedhewitt](https://github.com/reedhewitt)! + + +v2.0.0-rc.1 - 2021-06-15 +------------------------ + ++ Fixes issue encountered when adding a confirm group ++ Stop using merge codes in the password block ++ Improve block duplication handlers ++ Prevent confirm fields from being manually pasted outside of a confirm group ++ Adds the `llms/user-information-fields` redux store to allow for better field validation and handling ++ Improves and adds field attribute validation ++ Use rich text `allowedFormats` in favor of deprecated `formattingControls` ++ Remove the now unnecessary `uuid` field block attribute. ++ Adds WP core 5.8 compatibility on the widget and customizer screens. ++ Exclude LifterLMS field block reusables from the widgets reusable blocks screen. ++ Adds backwards compatibility for WordPress < 5.6 + + +v2.0.0-beta.6 - 2021-06-01 +-------------------------- + ++ (Re-)introduces user information shortcode through a block editor rich text area format button. ++ Prevent usage the "User Login" block on account edit forms (usersnames cannot be edited in WordPress). ++ Only prevent form posts from being made "draft" status on the "core" forms. ++ Modifies field localization data strategy for field validation and others. + + +v2.0.0-beta.5 - 2021-05-18 +-------------------------- + ++ Add WP core 5.8 compatibility for deprecated filter `block_categories`. ++ Fixed issue encountered on courses and memberships when attempting to edit instructor information. ++ Added validation to ensure all fields have unique HTML name attributes. ++ Simplified field data storage interface to enable saving only to the usermeta table. + + +v2.0.0-beta.4 - 2021-05-07 +-------------------------- + ++ Fixed error encountered when opening the block editor options menu on an `llms_form` post type. ++ Added UUID generation to all form field blocks. ++ Fixed visual issues encountered with form field blocks on wide screens in the block editor. ++ Fixed issue preventing column widths from being set after switching from a stacked layout to a columns layout for a field group. ++ Added CSS classes to various option elements in the block editor ++ Moved most inline css in the editor into a static file ++ Fixed issue encountered when reverting a form to it's default ++ Fixed dynamic block rendering errors encountered when the block is restricted to specific courses/memberships. ++ Added CSS to make input placeholder text look like a placeholder + + +v2.0.0-beta.3 - 2021-04-26 +-------------------------- + ++ All form field blocks refactored and many were removed or renamed. ++ Added column support to form field blocks. ++ Added reusable block support to form field blocks. ++ Removed support for block visibility on required field blocks (email and password). ++ Added reusable block filtering to only show "supported" reusable blocks when editing a form. ++ Added utility function support for reusable blocks. ++ Fixed issues related to visual rendering of checkboxes / radio elements on custom fields. + + +v2.0.0-beta.2 - 2021-03-22 +-------------------------- + ++ Fixed block editor visual issues encountered on certain blocks when block-level visibility restrictions are enabled. + + +v2.0.0-beta.1 - 2021-03-22 +-------------------------- + ++ Improved Javascript localization. ++ Updated JS source files to follow (slightly modified) eslint standards as defined by `@wordpress/eslint-plugin/recommended`. ++ Disabled import of incomplete module `./formats/merge-codes`. ++ Improved the information displayed for a restricted block. ++ Don't render `InspectorControls` for the Course Syllabus block since it doesn't have any actual settings to inspect. ++ Improved the Search, SearchPost, and SearchUser components and made backwards incompatible changes to their usage. + + +v1.12.0 - 2021-01-07 +-------------------- + ++ Various form and field updates in preparation for LifterLMS 5.0.0. + + +v1.11.1 - 2021-01-05 +-------------------- + ++ Update the hook used for the Instructors block when displayed on membership post types. + + +v1.11.0 - 2020-12-29 +-------------------- + ++ Allow the "Instructors" block to be used for memberships, thanks [@alaa-alshamy](https://github.com/alaa-alshamy)! + + +v1.10.0 - 2020-11-24 +-------------------- + ++ Use the `LLMS_Assets` class to define, register, and enqueue plugin assets. ++ Added Javascript localization for block editor scripts. + + +v1.9.1 - 2020-04-29 +------------------- + ++ Fix course progress block template used when migrating a course to the block editor. + + +v1.9.0 - 2020-04-29 +------------------- + ++ Converted the course progress block into a dynamic block. Fixes an issue allowing the progress block to be visible to non-enrolled students. ++ Added a filter on the output of the Pricing Table block: `llms_blocks_render_pricing_table_block`. + + +v1.8.0 - 2020-04-28 +------------------- + +##### Updates + ++ Improved script dependencies definitions. ++ Updated asset paths for consistency with other LifterLMS projects. ++ Updated various WP Core references that have been deprecated (maintains backwards compatibility). ++ The Lesson Progression block is no longer rendered server-side in the block editor (minor performance improvement). + +##### Changes to the Classic Editor Block + ++ 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. ++ 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. + +##### Bug fixes + ++ Fixed an issue encountered when using the WP Core "Table" block. ++ Fixed a few areas where `class` was being used instead of `className` to define CSS classes on elements in the block editor. ++ Fixed a user-experience issues encountered on the Course Information block when all possible information is disabled. ++ Fixed an issue causing visibility attributes to render on blocks that don't support them. ++ Fixed an issue preventing 3rd party blocks from modifying default block visibility settings. ++ Fixed a spelling error visible inside the block editor. ++ Fixed an issue causing the "Course Progress" block to be shown to non-enrolled students and visitors. ++ Removed redundant CSS from frontend. ++ Stop outputting editor CSS on the frontend. ++ Dynamic blocks with no content to render will now only output their empty render messages inside the block editor, not on the frontend. + + +v1.7.3 - 2019-12-19 +------------------- + ++ Move form ready event from domReady to block registration to ensure blocks are exposed before blocks are parsed. + + +v1.7.2 - 2019-12-09 +------------------- + ++ Bug fix: fix issue causing the block editor to encounter a fatal error when using custom post types that don't support custom fields. + + +v1.7.1 - 2019-12-05 +------------------- + ++ Bug fix: Fixed a WordPress 5.3 issues with JSON data affecting the ability to save course/membership instructors. ++ Update: Added filter, `llms_block_supports_visibility` to allow modification of the return of the check. ++ Update: Disabled block visibility on registration & account forms to prevent a potentially confusing form creation experience. ++ Update: Added block editor rendering for password type fields. + + +v1.7.0 - 2019-11-08 +------------------- + +##### Updates + ++ Membership post types can now use the LifterLMS Pricing Table block. ++ Membership post types are automatically migrated to the block editor (use the pricing table block instead of the pricing table action). ++ Added a block editor template for the Membership post type. ++ The block 'llms/form-field-redeem-voucher' is now only available on registration forms. + +##### Bug Fixes + ++ Backwards compatibility fixes for WP Core 5.2 and earlier. ++ Perform post migrations on `current_screen` instead of `admin_enqueue_scripts`. ++ Fix 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. ++ Import `InspectorControls` from `wp.blockEditor` and fallback to `wp.editor` to maintain backwards compatibility. ++ Fall back to `wp.editor` for `RichText` import when `wp.blockEditor` is not found. ++ Import from `wp.editor` when `wp.blockEditor` is not available. ++ Return early during renders on WP Core 5.2 and earlier where the `PluginDocumentSettingPanel` doesn't exist. + + +v1.6.0 - 2019-10-24 +------------------- + ++ Feature: Added form field blocks for use on the Forms manager. ++ Feature: Add logic for `logged_in` and `logged_out` block visibility options. ++ Update: Added isDisabled property to Search component. ++ Update: Adjusted priority of `render_block` filter to 20. ++ Bug fix: Import `InspectorControls` from `wp.blockEditor` in favor of deprecated `wp.editor` ++ Bug fix: Automatically store course/membership instructor with `post_author` data when the post is created. ++ Bug fix: Pass style rules as camelCase. + + +v1.5.2 - 2019-08-14 +------------------- + ++ Only enable REST for authenticated users with the `lifterlms_instructor` capability. + + +v1.5.1 - 2019-05-17 +------------------- + ++ 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. + + +v1.5.0 - 2019-05-16 +------------------- + ++ All blocks are now registered only for post types where they can actually be used. + + +v1.4.1 - 2019-05-13 +------------------- + ++ Fixed double slashes in asset path of CSS and JS files, thanks [@pondermatic](https://github.com/pondermatic)! + + +v1.4.0 - 2019-04-26 +------------------- + ++ Added 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. 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. + + +v1.3.8 - 2019-03-19 +------------------- + ++ Explicitly import jQuery when used within blocks. + + +v1.3.7 - 2019-02-27 +------------------- + ++ 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. + + +v1.3.6 - 2019-02-22 +------------------- + ++ Updated the editor icons to use the new LifterLMS Icon ++ Change method for Pricing Table block re-rendering to prevent an issue resulting it always appearing that the post has unsaved data. + + +v1.3.5 - 2019-02-21 +------------------- + ++ Automatically re-renders Pricing Table blocks when access plans are saved or deleted via the course / membership access plan metabox. + + +v1.3.4 - 2019-01-30 +------------------- + ++ Add support for the Divi Builder's "Classic Editor" mode ++ Skip post migration when "Classic" mode is enabled + + +v1.3.3 - 2019-01-23 +------------------- + ++ Add conditions to check for Classic Editor settings configured to enforce classic/block for all posts. + + +v1.3.2 - 2019-01-16 +------------------- + ++ Fix issue preventing template actions from being removed from migrated courses & lessons. + + +v1.3.1 - 2019-01-15 +------------------- + ++ Move post migration checks to a callable function `llms_blocks_is_post_migrated()` + + +v1.3.0 - 2019-01-09 +------------------- + ++ Add course and membership catalog visibility settings into the block editor. ++ Fixed issue preventing the course instructors metabox from displaying when using the classic editor plugin. + +v1.2.0 - 2018-12-27 +------------------- + ++ Add conditional support for page builders: Beaver Builder, Divi Builder, and Elementor. ++ Fixed issue causing LifterLMS core sales pages from outputting automatic content (like pricing tables) on migrated posts. + + +v1.1.2 - 2018-12-17 +------------------- + ++ Add a filter to the migration check on lessons & courses. + + +v1.1.1 - 2018-12-14 +------------------- + ++ Fix issue causing LifterLMS Core Actions to be removed when using the Classic Editor plugin. + + +v1.1.0 - 2018-12-12 +------------------- + ++ Editor blocks now display a lock icon when hovering/selecting a block which corresponds to the enrollment visibility settings of the block. ++ Removal of core actions is now handled by a general migrator function instead of by individual blocks. ++ Fix issue causing block visibility options to not be properly set when enrollment visibility is first enabled for a block. + + +v1.0.1 - 2018-12-05 +------------------- + ++ Made plugin url relative + + +v1.0.0 - 2018-12-05 +------------------- + ++ Initial public release diff --git a/libraries/lifterlms-blocks/assets/css/llms-blocks-rtl.css b/libraries/lifterlms-blocks/assets/css/llms-blocks-rtl.css new file mode 100644 index 0000000000..f56150261d --- /dev/null +++ b/libraries/lifterlms-blocks/assets/css/llms-blocks-rtl.css @@ -0,0 +1 @@ +.llms-cols:after,.llms-cols:before{content:" ";display:table}.llms-cols:after{clear:both}.llms-cols .llms-col{width:100%}@media (min-width:600px){.llms-cols [class*=llms-col-]{float:right}.llms-cols .llms-col-1{width:100%}.llms-cols .llms-col-2{width:50%}.llms-cols .llms-col-3{width:33.3333333333%}.llms-cols .llms-col-4{width:25%}.llms-cols .llms-col-5{width:20%}.llms-cols .llms-col-6{width:16.6666666667%}.llms-cols .llms-col-7{width:14.2857142857%}.llms-cols .llms-col-8{width:12.5%}.llms-cols .llms-col-9{width:11.1111111111%}.llms-cols .llms-col-10{width:10%}.llms-cols .llms-col-11{width:9.0909090909%}.llms-cols .llms-col-12{width:8.3333333333%}}@media(min-width:600px){.edit-post-visual-editor .editor-block-list__block .editor-block-list__block-edit{padding-right:0;padding-left:0}}.llms-block-visibility{margin-right:auto;margin-left:auto;max-width:840px;position:relative}.llms-block-visibility>:first-child{margin-bottom:28px;margin-top:28px}.llms-block-visibility:before{border:1px solid #e0e0e0;bottom:-6px;content:"";right:-6px;position:absolute;left:-6px;top:-6px}.llms-block-visibility .llms-block-visibility--indicator{color:#555d66;border-top:1px solid #e0e0e0;margin-top:-22px;padding:0 6px}.llms-block-visibility .llms-block-visibility--indicator .dashicon,.llms-block-visibility .llms-block-visibility--indicator .llms-block-visibility--msg{vertical-align:middle}.llms-block-visibility .llms-block-visibility--indicator .llms-block-visibility--msg{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:13px;font-style:italic;line-height:1.4;margin-right:6px}.edit-post-settings-sidebar__panel-block .components-panel__body .llms-search input,.edit-post-sidebar .components-panel__body .llms-search input{box-shadow:none}.llms-search__menu{background:#fff!important;z-index:9999999!important}.llms-search__value-container{width:100%}#wpwrap .edit-post-visual-editor .wp-block-llms-course-information ul{list-style-type:none;margin-right:0;margin-top:.5em}.wp-block-llms-course-progress{display:flex}.wp-block-llms-course-progress .progress-bar{background:#dedede;border-radius:4px;flex:1;margin:10px 0;overflow:hidden}.wp-block-llms-course-progress .progress-bar .progress--fill{background:#2295ff;height:100%;width:50%}.wp-block-llms-course-progress span{padding-right:5px;vertical-align:middle}.llms-syllabus-wrapper{margin:15px;text-align:center}.llms-syllabus-wrapper .llms-section-title{margin:25px 0 0}.llms-course-navigation:after,.llms-course-navigation:before{content:" ";display:table}.llms-course-navigation:after{clear:both}.llms-course-navigation .llms-back-to-course,.llms-course-navigation .llms-next-lesson,.llms-course-navigation .llms-prev-lesson{width:49%}.llms-course-navigation .llms-back-to-course,.llms-course-navigation .llms-prev-lesson{float:right;margin-left:.5%}.llms-course-navigation .llms-next-lesson,.llms-course-navigation .llms-prev-lesson+.llms-back-to-course{float:left;margin-right:.5%}.llms-lesson-preview{display:inline-block;margin-top:15px;max-width:100%;position:relative;width:480px}.llms-lesson-preview .llms-lesson-link{background:#f1f1f1;color:#212121;display:block;padding:15px;text-decoration:none}.llms-lesson-preview .llms-lesson-link:after,.llms-lesson-preview .llms-lesson-link:before{content:" ";display:table}.llms-lesson-preview .llms-lesson-link:after{clear:both}.llms-lesson-preview .llms-lesson-link:hover{background:#eaeaea}.llms-lesson-preview .llms-lesson-link:visited{color:#212121}.llms-lesson-preview .llms-lesson-thumbnail{margin-bottom:10px}.llms-lesson-preview .llms-lesson-thumbnail img{display:block;width:100%}.llms-lesson-preview .llms-pre-text{text-align:right}.llms-lesson-preview .llms-lesson-title{font-weight:700;margin:0 auto 10px;text-align:right}.llms-lesson-preview .llms-lesson-title:last-child{margin-bottom:0}.llms-lesson-preview .llms-lesson-excerpt{text-align:right}.llms-lesson-preview .llms-main{float:right;width:100%}.llms-lesson-preview .llms-extra{float:left;width:15%}.llms-lesson-preview .llms-extra+.llms-main{width:85%}.llms-lesson-preview .llms-free-lesson-svg,.llms-lesson-preview .llms-lesson-complete,.llms-lesson-preview .llms-lesson-complete-placeholder,.llms-lesson-preview .llms-lesson-counter{display:block;font-size:32px;margin-bottom:15px}.llms-lesson-preview.is-complete .llms-lesson-complete,.llms-lesson-preview.is-free .llms-lesson-complete{color:#2295ff}.llms-lesson-preview .llms-icon-free{background:#2295ff;border-radius:4px;color:#f1f1f1;display:inline-block;padding:5px 6px 4px;line-height:1;font-size:14px}.llms-lesson-preview.is-incomplete .llms-lesson-complete{color:#cacaca}.llms-lesson-preview .llms-lesson-counter{font-size:16px;line-height:1}.llms-lesson-preview .llms-free-lesson-svg{fill:currentColor;height:23px;width:50px}.llms-lesson-preview p{margin-bottom:0;margin-top:0}.llms-author .label,.llms-author .name{margin-right:5px}.llms-author .avatar{border-radius:50%}.llms-author .bio{margin-top:5px}.llms-instructor-info .llms-instructors .llms-col:first-child .llms-author{margin-right:0}.llms-instructor-info .llms-instructors .llms-col:last-child .llms-author{margin-left:0}.llms-instructor-info .llms-instructors .llms-author{background:#f5f5f5;border-top:4px solid #2295ff;text-align:center;margin:45px 5px 5px;padding:0 10px 10px}.llms-instructor-info .llms-instructors .llms-author .avatar{background:#2295ff;border:4px solid #2295ff;display:block;margin:-35px auto 10px}.llms-instructor-info .llms-instructors .llms-author .llms-author-info{display:block}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.name{font-weight:700}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.label{font-size:85%}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.bio{font-size:90%;margin-bottom:0}.wp-block[data-type="llms/lesson-progression"]{text-align:center}.wp-block[data-type="llms/lesson-progression"] button{margin:0 2px}.llms-access-plans:after,.llms-access-plans:before{content:" ";display:table}.llms-access-plans:after{clear:both}@media (min-width:600px){.llms-access-plans.cols-1 .llms-access-plan{width:100%}.llms-access-plans.cols-2 .llms-access-plan{width:50%}.llms-access-plans.cols-3 .llms-access-plan{width:33.3333333333%}.llms-access-plans.cols-4 .llms-access-plan{width:25%}.llms-access-plans.cols-5 .llms-access-plan{width:20%}}.llms-free-enroll-form{margin-bottom:0}.llms-access-plan{box-sizing:border-box;float:right;text-align:center;width:100%}.llms-access-plan .llms-access-plan-content,.llms-access-plan .llms-access-plan-footer{background:#f1f1f1}.llms-access-plan.featured .llms-access-plan-featured{background:#4ba9ff}.llms-access-plan.featured .llms-access-plan-content,.llms-access-plan.featured .llms-access-plan-footer{border-right:3px solid #2295ff;border-left:3px solid #2295ff}.llms-access-plan.featured .llms-access-plan-footer{border-bottom-color:#2295ff}.llms-access-plan.on-sale .price-regular{text-decoration:line-through}.llms-access-plan .stamp{background:#2295ff;color:#fff;font-size:11px;font-style:normal;font-weight:300;padding:2px 3px;vertical-align:top}.llms-access-plan .llms-access-plan-restrictions ul{margin:0}.llms-access-plan-featured{color:#fff;font-size:14px;font-weight:400;margin:0 2px}.llms-access-plan-content{margin:0 2px}.llms-access-plan-content .llms-access-plan-pricing{padding:10px 0 0}.llms-access-plan-title{background:#2295ff;color:#fff;margin-bottom:0;padding:10px}.llms-access-plan-pricing .llms-price-currency-symbol{font-size:14px;vertical-align:top}.llms-access-plan-price{font-size:18px;font-variant:small-caps;line-height:20px}.llms-access-plan-price .lifterlms-price{font-weight:700}.llms-access-plan-price.sale{padding:5px 0;border-top:1px solid #d0d0d0;border-bottom:1px solid #d0d0d0}.llms-access-plan-expiration,.llms-access-plan-sale-end,.llms-access-plan-schedule,.llms-access-plan-trial{font-size:15px;font-variant:small-caps;line-height:1.2}.llms-access-plan-description{font-size:16px;padding:10px 10px 0}.llms-access-plan-description ul{margin:0}.llms-access-plan-description ul li{border-bottom:1px solid #d0d0d0;list-style-type:none}.llms-access-plan-description ul li:last-child{border-bottom:none}.llms-access-plan-description div:last-child,.llms-access-plan-description img:last-child,.llms-access-plan-description li:last-child,.llms-access-plan-description p:last-child,.llms-access-plan-description ul:last-child{margin-bottom:0}.llms-access-plan-restrictions .stamp{vertical-align:baseline}.llms-access-plan-restrictions ul{margin:0}.llms-access-plan-restrictions ul li{font-size:12px;line-height:14px;list-style-type:none}.llms-access-plan-restrictions a{color:#f8954f}.llms-access-plan-restrictions a:hover{color:#f67d28}.llms-access-plan-footer{border-bottom:3px solid #f1f1f1;padding:10px;margin:0 2px 2px}.llms-access-plan-footer .llms-access-plan-pricing{padding:0 0 10px}.llms-invalid-control{margin-bottom:24px}.llms-invalid-control .components-base-control{margin-bottom:0}.llms-invalid-control .components-base-control .components-text-control__input{border-color:#cc1818;background-color:rgba(204,24,24,.05)}.llms-invalid-control .llms-invalid-control--msg{background-color:rgba(204,24,24,.05);border-right:4px solid #cc1818;color:#cc1818;font-style:italic;font-size:12px;margin-bottom:0;padding:6px 8px 6px 2px}.llms-pwd-meter{border:1px solid #e35b5b;margin-top:5px;border-radius:4px;overflow:hidden}.llms-pwd-meter>div{background:rgba(227,91,91,.25);font-size:75%;padding:0 5px;width:25%}.llms-fields input,.llms-fields textarea{border:1px solid #999;color:#757575;padding:4px 8px}.llms-fields input:focus::-moz-placeholder,.llms-fields textarea:focus::-moz-placeholder{opacity:0}.llms-fields input:focus:-ms-input-placeholder,.llms-fields textarea:focus:-ms-input-placeholder{opacity:0}.llms-fields input:focus::placeholder,.llms-fields textarea:focus::placeholder{opacity:0}.llms-fields input::-moz-placeholder,.llms-fields textarea::-moz-placeholder{color:#757575}.llms-fields input:-ms-input-placeholder,.llms-fields textarea:-ms-input-placeholder{color:#757575}.llms-fields input::placeholder,.llms-fields textarea::placeholder{color:#757575}.llms-fields input:not([type=radio]):not([type=checkbox]),.llms-fields select,.llms-fields textarea{width:100%}.llms-fields input:not([type=radio]){border-radius:4px}.llms-fields textarea{resize:none}.llms-fields select{max-Width:none;pointer-events:none}.llms-fields .llms-field .block-editor-rich-text__editable{display:block}.llms-fields .llms-field label.llms-is-required>div{display:inline}.llms-fields .llms-field label.llms-is-required:after{content:" *";color:#dc5757}.llms-field-option{display:flex;align-items:top;margin-bottom:4px}.llms-field-option.llms-sort-helper{background:#fff;border:1px solid #dedede;height:auto!important;padding:5px 10px;z-index:999}.llms-field-option .llms-field-opt-default{margin-top:6px}.llms-field-option .llms-field-opt-default .components-radio-control__input{margin-left:0}.llms-field-option .llms-field-opt-default,.llms-field-option .llms-field-opt-text,.llms-field-option .llms-field-opt-text .components-base-control__field{margin-bottom:0!important}.llms-field-option .llms-field-opt-db-key{display:flex;margin-top:2px}.llms-field-option .llms-field-opt-db-key .dashicon{margin-top:5px;color:#5a5a5a}.llms-field-option .llms-field-opt-db-key .components-text-control__input{background:#f5f5f5;font-family:monospace}.llms-field-option .llms-drag-handle{cursor:-webkit-grab;cursor:grab;flex:.8;padding-top:6px;margin-top:3px}.llms-field-option .llms-del-field-opt-wrap,.llms-field-option .llms-field-opt-default-wrap{flex:1;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.llms-field-option .llms-del-field-opt-wrap{margin-right:4px}.llms-field-option .llms-del-field-opt-wrap button{margin-top:3px}.llms-field-option .llms-del-field-opt-wrap button:hover,.llms-field-option .llms-del-field-opt-wrap button[aria-expanded=true]{color:#cc1818}.llms-field-option .llms-field-opt-text-wrap{flex:7}.llms-field-options--footer{margin-top:10px}.llms-cols-12 .llms-field{width:100%}.llms-cols-9 .llms-field{width:75%}.llms-cols-8 .llms-field{width:66.66%}.llms-cols-6 .llms-field{width:50%}.llms-cols-4 .llms-field{width:33.33%}.llms-cols-3 .llms-field{width:25%}.llms-field-group[data-field-layout=columns] .llms-cols-12,.llms-field-group[data-field-layout=columns] [class*=llms-cols-] .llms-field{width:100%}.llms-field-group[data-field-layout=columns] .llms-cols-9{width:75%}.llms-field-group[data-field-layout=columns] .llms-cols-8{width:66.66%}.llms-field-group[data-field-layout=columns] .llms-cols-6{width:50%}.llms-field-group[data-field-layout=columns] .llms-cols-4{width:33.33%}.llms-field-group[data-field-layout=columns] .llms-cols-3{width:25%}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields{display:inline-block}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields:nth-child(odd){padding-left:28px}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields:nth-child(2n){padding-right:28px}.llms-shortcodes-modal{width:800px}.llms-shortcodes-modal .llms-shortcodes-modal--main{display:flex}.llms-shortcodes-modal .llms-shortcodes-modal--main aside{flex:1;padding-left:16px}.llms-shortcodes-modal .llms-shortcodes-modal--main section{flex:2;padding-right:16px}.llms-shortcodes-modal .llms-shortcodes-modal--main .llms-table tr td,.llms-shortcodes-modal .llms-shortcodes-modal--main .llms-table tr th{text-align:right}.llms-instructor{border:1px solid #dedede;margin-bottom:-1px;padding:10px;position:relative;z-index:100}.llms-instructor .llms-instructor--header{display:flex;align-items:center}.llms-instructor .llms-instructor--header section{flex:2}.llms-instructor .llms-instructor--header section small{margin-right:3px}.llms-instructor .llms-instructor--header aside{flex:1;text-align:left}.llms-instructor .llms-instructor--header .components-button.is-small.has-icon:not(.has-text){min-width:24px;padding:0}.llms-instructor .llms-instructor--header .dashicons-star-filled{color:#ffb900;margin:2px 0 0 2px}.llms-instructor.llms-is-dragging{box-shadow:0 4px 8px 2px #dedede;border:1px solid #dedede;background:#fff;z-index:999}.llms-instructor .llms-instructor--settings{margin-top:10px} \ No newline at end of file diff --git a/libraries/lifterlms-blocks/assets/css/llms-blocks.css b/libraries/lifterlms-blocks/assets/css/llms-blocks.css new file mode 100644 index 0000000000..a2c3cccb7c --- /dev/null +++ b/libraries/lifterlms-blocks/assets/css/llms-blocks.css @@ -0,0 +1 @@ +.llms-cols:after,.llms-cols:before{content:" ";display:table}.llms-cols:after{clear:both}.llms-cols .llms-col{width:100%}@media (min-width:600px){.llms-cols [class*=llms-col-]{float:left}.llms-cols .llms-col-1{width:100%}.llms-cols .llms-col-2{width:50%}.llms-cols .llms-col-3{width:33.3333333333%}.llms-cols .llms-col-4{width:25%}.llms-cols .llms-col-5{width:20%}.llms-cols .llms-col-6{width:16.6666666667%}.llms-cols .llms-col-7{width:14.2857142857%}.llms-cols .llms-col-8{width:12.5%}.llms-cols .llms-col-9{width:11.1111111111%}.llms-cols .llms-col-10{width:10%}.llms-cols .llms-col-11{width:9.0909090909%}.llms-cols .llms-col-12{width:8.3333333333%}}@media(min-width:600px){.edit-post-visual-editor .editor-block-list__block .editor-block-list__block-edit{padding-left:0;padding-right:0}}.llms-block-visibility{margin-left:auto;margin-right:auto;max-width:840px;position:relative}.llms-block-visibility>:first-child{margin-bottom:28px;margin-top:28px}.llms-block-visibility:before{border:1px solid #e0e0e0;bottom:-6px;content:"";left:-6px;position:absolute;right:-6px;top:-6px}.llms-block-visibility .llms-block-visibility--indicator{color:#555d66;border-top:1px solid #e0e0e0;margin-top:-22px;padding:0 6px}.llms-block-visibility .llms-block-visibility--indicator .dashicon,.llms-block-visibility .llms-block-visibility--indicator .llms-block-visibility--msg{vertical-align:middle}.llms-block-visibility .llms-block-visibility--indicator .llms-block-visibility--msg{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:13px;font-style:italic;line-height:1.4;margin-left:6px}.edit-post-settings-sidebar__panel-block .components-panel__body .llms-search input,.edit-post-sidebar .components-panel__body .llms-search input{box-shadow:none}.llms-search__menu{background:#fff!important;z-index:9999999!important}.llms-search__value-container{width:100%}#wpwrap .edit-post-visual-editor .wp-block-llms-course-information ul{list-style-type:none;margin-left:0;margin-top:.5em}.wp-block-llms-course-progress{display:flex}.wp-block-llms-course-progress .progress-bar{background:#dedede;border-radius:4px;flex:1;margin:10px 0;overflow:hidden}.wp-block-llms-course-progress .progress-bar .progress--fill{background:#2295ff;height:100%;width:50%}.wp-block-llms-course-progress span{padding-left:5px;vertical-align:middle}.llms-syllabus-wrapper{margin:15px;text-align:center}.llms-syllabus-wrapper .llms-section-title{margin:25px 0 0}.llms-course-navigation:after,.llms-course-navigation:before{content:" ";display:table}.llms-course-navigation:after{clear:both}.llms-course-navigation .llms-back-to-course,.llms-course-navigation .llms-next-lesson,.llms-course-navigation .llms-prev-lesson{width:49%}.llms-course-navigation .llms-back-to-course,.llms-course-navigation .llms-prev-lesson{float:left;margin-right:.5%}.llms-course-navigation .llms-next-lesson,.llms-course-navigation .llms-prev-lesson+.llms-back-to-course{float:right;margin-left:.5%}.llms-lesson-preview{display:inline-block;margin-top:15px;max-width:100%;position:relative;width:480px}.llms-lesson-preview .llms-lesson-link{background:#f1f1f1;color:#212121;display:block;padding:15px;text-decoration:none}.llms-lesson-preview .llms-lesson-link:after,.llms-lesson-preview .llms-lesson-link:before{content:" ";display:table}.llms-lesson-preview .llms-lesson-link:after{clear:both}.llms-lesson-preview .llms-lesson-link:hover{background:#eaeaea}.llms-lesson-preview .llms-lesson-link:visited{color:#212121}.llms-lesson-preview .llms-lesson-thumbnail{margin-bottom:10px}.llms-lesson-preview .llms-lesson-thumbnail img{display:block;width:100%}.llms-lesson-preview .llms-pre-text{text-align:left}.llms-lesson-preview .llms-lesson-title{font-weight:700;margin:0 auto 10px;text-align:left}.llms-lesson-preview .llms-lesson-title:last-child{margin-bottom:0}.llms-lesson-preview .llms-lesson-excerpt{text-align:left}.llms-lesson-preview .llms-main{float:left;width:100%}.llms-lesson-preview .llms-extra{float:right;width:15%}.llms-lesson-preview .llms-extra+.llms-main{width:85%}.llms-lesson-preview .llms-free-lesson-svg,.llms-lesson-preview .llms-lesson-complete,.llms-lesson-preview .llms-lesson-complete-placeholder,.llms-lesson-preview .llms-lesson-counter{display:block;font-size:32px;margin-bottom:15px}.llms-lesson-preview.is-complete .llms-lesson-complete,.llms-lesson-preview.is-free .llms-lesson-complete{color:#2295ff}.llms-lesson-preview .llms-icon-free{background:#2295ff;border-radius:4px;color:#f1f1f1;display:inline-block;padding:5px 6px 4px;line-height:1;font-size:14px}.llms-lesson-preview.is-incomplete .llms-lesson-complete{color:#cacaca}.llms-lesson-preview .llms-lesson-counter{font-size:16px;line-height:1}.llms-lesson-preview .llms-free-lesson-svg{fill:currentColor;height:23px;width:50px}.llms-lesson-preview p{margin-bottom:0;margin-top:0}.llms-author .label,.llms-author .name{margin-left:5px}.llms-author .avatar{border-radius:50%}.llms-author .bio{margin-top:5px}.llms-instructor-info .llms-instructors .llms-col:first-child .llms-author{margin-left:0}.llms-instructor-info .llms-instructors .llms-col:last-child .llms-author{margin-right:0}.llms-instructor-info .llms-instructors .llms-author{background:#f5f5f5;border-top:4px solid #2295ff;text-align:center;margin:45px 5px 5px;padding:0 10px 10px}.llms-instructor-info .llms-instructors .llms-author .avatar{background:#2295ff;border:4px solid #2295ff;display:block;margin:-35px auto 10px}.llms-instructor-info .llms-instructors .llms-author .llms-author-info{display:block}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.name{font-weight:700}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.label{font-size:85%}.llms-instructor-info .llms-instructors .llms-author .llms-author-info.bio{font-size:90%;margin-bottom:0}.wp-block[data-type="llms/lesson-progression"]{text-align:center}.wp-block[data-type="llms/lesson-progression"] button{margin:0 2px}.llms-access-plans:after,.llms-access-plans:before{content:" ";display:table}.llms-access-plans:after{clear:both}@media (min-width:600px){.llms-access-plans.cols-1 .llms-access-plan{width:100%}.llms-access-plans.cols-2 .llms-access-plan{width:50%}.llms-access-plans.cols-3 .llms-access-plan{width:33.3333333333%}.llms-access-plans.cols-4 .llms-access-plan{width:25%}.llms-access-plans.cols-5 .llms-access-plan{width:20%}}.llms-free-enroll-form{margin-bottom:0}.llms-access-plan{box-sizing:border-box;float:left;text-align:center;width:100%}.llms-access-plan .llms-access-plan-content,.llms-access-plan .llms-access-plan-footer{background:#f1f1f1}.llms-access-plan.featured .llms-access-plan-featured{background:#4ba9ff}.llms-access-plan.featured .llms-access-plan-content,.llms-access-plan.featured .llms-access-plan-footer{border-left:3px solid #2295ff;border-right:3px solid #2295ff}.llms-access-plan.featured .llms-access-plan-footer{border-bottom-color:#2295ff}.llms-access-plan.on-sale .price-regular{text-decoration:line-through}.llms-access-plan .stamp{background:#2295ff;color:#fff;font-size:11px;font-style:normal;font-weight:300;padding:2px 3px;vertical-align:top}.llms-access-plan .llms-access-plan-restrictions ul{margin:0}.llms-access-plan-featured{color:#fff;font-size:14px;font-weight:400;margin:0 2px}.llms-access-plan-content{margin:0 2px}.llms-access-plan-content .llms-access-plan-pricing{padding:10px 0 0}.llms-access-plan-title{background:#2295ff;color:#fff;margin-bottom:0;padding:10px}.llms-access-plan-pricing .llms-price-currency-symbol{font-size:14px;vertical-align:top}.llms-access-plan-price{font-size:18px;font-variant:small-caps;line-height:20px}.llms-access-plan-price .lifterlms-price{font-weight:700}.llms-access-plan-price.sale{padding:5px 0;border-top:1px solid #d0d0d0;border-bottom:1px solid #d0d0d0}.llms-access-plan-expiration,.llms-access-plan-sale-end,.llms-access-plan-schedule,.llms-access-plan-trial{font-size:15px;font-variant:small-caps;line-height:1.2}.llms-access-plan-description{font-size:16px;padding:10px 10px 0}.llms-access-plan-description ul{margin:0}.llms-access-plan-description ul li{border-bottom:1px solid #d0d0d0;list-style-type:none}.llms-access-plan-description ul li:last-child{border-bottom:none}.llms-access-plan-description div:last-child,.llms-access-plan-description img:last-child,.llms-access-plan-description li:last-child,.llms-access-plan-description p:last-child,.llms-access-plan-description ul:last-child{margin-bottom:0}.llms-access-plan-restrictions .stamp{vertical-align:baseline}.llms-access-plan-restrictions ul{margin:0}.llms-access-plan-restrictions ul li{font-size:12px;line-height:14px;list-style-type:none}.llms-access-plan-restrictions a{color:#f8954f}.llms-access-plan-restrictions a:hover{color:#f67d28}.llms-access-plan-footer{border-bottom:3px solid #f1f1f1;padding:10px;margin:0 2px 2px}.llms-access-plan-footer .llms-access-plan-pricing{padding:0 0 10px}.llms-invalid-control{margin-bottom:24px}.llms-invalid-control .components-base-control{margin-bottom:0}.llms-invalid-control .components-base-control .components-text-control__input{border-color:#cc1818;background-color:rgba(204,24,24,.05)}.llms-invalid-control .llms-invalid-control--msg{background-color:rgba(204,24,24,.05);border-left:4px solid #cc1818;color:#cc1818;font-style:italic;font-size:12px;margin-bottom:0;padding:6px 2px 6px 8px}.llms-pwd-meter{border:1px solid #e35b5b;margin-top:5px;border-radius:4px;overflow:hidden}.llms-pwd-meter>div{background:rgba(227,91,91,.25);font-size:75%;padding:0 5px;width:25%}.llms-fields input,.llms-fields textarea{border:1px solid #999;color:#757575;padding:4px 8px}.llms-fields input:focus::-moz-placeholder,.llms-fields textarea:focus::-moz-placeholder{opacity:0}.llms-fields input:focus:-ms-input-placeholder,.llms-fields textarea:focus:-ms-input-placeholder{opacity:0}.llms-fields input:focus::placeholder,.llms-fields textarea:focus::placeholder{opacity:0}.llms-fields input::-moz-placeholder,.llms-fields textarea::-moz-placeholder{color:#757575}.llms-fields input:-ms-input-placeholder,.llms-fields textarea:-ms-input-placeholder{color:#757575}.llms-fields input::placeholder,.llms-fields textarea::placeholder{color:#757575}.llms-fields input:not([type=radio]):not([type=checkbox]),.llms-fields select,.llms-fields textarea{width:100%}.llms-fields input:not([type=radio]){border-radius:4px}.llms-fields textarea{resize:none}.llms-fields select{max-Width:none;pointer-events:none}.llms-fields .llms-field .block-editor-rich-text__editable{display:block}.llms-fields .llms-field label.llms-is-required>div{display:inline}.llms-fields .llms-field label.llms-is-required:after{content:" *";color:#dc5757}.llms-field-option{display:flex;align-items:top;margin-bottom:4px}.llms-field-option.llms-sort-helper{background:#fff;border:1px solid #dedede;height:auto!important;padding:5px 10px;z-index:999}.llms-field-option .llms-field-opt-default{margin-top:6px}.llms-field-option .llms-field-opt-default .components-radio-control__input{margin-right:0}.llms-field-option .llms-field-opt-default,.llms-field-option .llms-field-opt-text,.llms-field-option .llms-field-opt-text .components-base-control__field{margin-bottom:0!important}.llms-field-option .llms-field-opt-db-key{display:flex;margin-top:2px}.llms-field-option .llms-field-opt-db-key .dashicon{margin-top:5px;color:#5a5a5a}.llms-field-option .llms-field-opt-db-key .components-text-control__input{background:#f5f5f5;font-family:monospace}.llms-field-option .llms-drag-handle{cursor:-webkit-grab;cursor:grab;flex:.8;padding-top:6px;margin-top:3px}.llms-field-option .llms-del-field-opt-wrap,.llms-field-option .llms-field-opt-default-wrap{flex:1;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.llms-field-option .llms-del-field-opt-wrap{margin-left:4px}.llms-field-option .llms-del-field-opt-wrap button{margin-top:3px}.llms-field-option .llms-del-field-opt-wrap button:hover,.llms-field-option .llms-del-field-opt-wrap button[aria-expanded=true]{color:#cc1818}.llms-field-option .llms-field-opt-text-wrap{flex:7}.llms-field-options--footer{margin-top:10px}.llms-cols-12 .llms-field{width:100%}.llms-cols-9 .llms-field{width:75%}.llms-cols-8 .llms-field{width:66.66%}.llms-cols-6 .llms-field{width:50%}.llms-cols-4 .llms-field{width:33.33%}.llms-cols-3 .llms-field{width:25%}.llms-field-group[data-field-layout=columns] .llms-cols-12,.llms-field-group[data-field-layout=columns] [class*=llms-cols-] .llms-field{width:100%}.llms-field-group[data-field-layout=columns] .llms-cols-9{width:75%}.llms-field-group[data-field-layout=columns] .llms-cols-8{width:66.66%}.llms-field-group[data-field-layout=columns] .llms-cols-6{width:50%}.llms-field-group[data-field-layout=columns] .llms-cols-4{width:33.33%}.llms-field-group[data-field-layout=columns] .llms-cols-3{width:25%}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields{display:inline-block}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields:nth-child(odd){padding-right:28px}.llms-field-group[data-field-layout=columns] .block-editor-block-list__layout>.wp-block.llms-fields:nth-child(2n){padding-left:28px}.llms-shortcodes-modal{width:800px}.llms-shortcodes-modal .llms-shortcodes-modal--main{display:flex}.llms-shortcodes-modal .llms-shortcodes-modal--main aside{flex:1;padding-right:16px}.llms-shortcodes-modal .llms-shortcodes-modal--main section{flex:2;padding-left:16px}.llms-shortcodes-modal .llms-shortcodes-modal--main .llms-table tr td,.llms-shortcodes-modal .llms-shortcodes-modal--main .llms-table tr th{text-align:left}.llms-instructor{border:1px solid #dedede;margin-bottom:-1px;padding:10px;position:relative;z-index:100}.llms-instructor .llms-instructor--header{display:flex;align-items:center}.llms-instructor .llms-instructor--header section{flex:2}.llms-instructor .llms-instructor--header section small{margin-left:3px}.llms-instructor .llms-instructor--header aside{flex:1;text-align:right}.llms-instructor .llms-instructor--header .components-button.is-small.has-icon:not(.has-text){min-width:24px;padding:0}.llms-instructor .llms-instructor--header .dashicons-star-filled{color:#ffb900;margin:2px 2px 0 0}.llms-instructor.llms-is-dragging{box-shadow:0 4px 8px 2px #dedede;border:1px solid #dedede;background:#fff;z-index:999}.llms-instructor .llms-instructor--settings{margin-top:10px} \ No newline at end of file diff --git a/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.asset.php b/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.asset.php new file mode 100644 index 0000000000..eb1e21bada --- /dev/null +++ b/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.asset.php @@ -0,0 +1 @@ +<?php return array('dependencies' => array('lodash', 'wp-polyfill', 'wp-redux-routine'), 'version' => '3522d2a2e5e9bf8f231f96c47d7fb93b'); \ No newline at end of file diff --git a/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.js b/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.js new file mode 100644 index 0000000000..5704248a3d --- /dev/null +++ b/libraries/lifterlms-blocks/assets/js/llms-blocks-backwards-compat.js @@ -0,0 +1 @@ +!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=76)}({17:function(e,t,r){"use strict";function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}r.d(t,"a",(function(){return n}))},22:function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function i(e,t){var r=e._map,n=e._arrayTreeMap,o=e._objectTreeMap;if(r.has(t))return r.get(t);for(var i=Object.keys(t).sort(),s=Array.isArray(t)?n:o,c=0;c<i.length;c++){var u=i[c];if(void 0===(s=s.get(u)))return;var a=t[u];if(void 0===(s=s.get(a)))return}var l=s.get("_ekm_value");return l?(r.delete(l[0]),l[0]=t,s.set("_ekm_value",l),r.set(t,l),l):void 0}var s=function(){function e(t){if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.clear(),t instanceof e){var r=[];t.forEach((function(e,t){r.push([t,e])})),t=r}if(null!=t)for(var n=0;n<t.length;n++)this.set(t[n][0],t[n][1])}var t,r;return t=e,(r=[{key:"set",value:function(t,r){if(null===t||"object"!==n(t))return this._map.set(t,r),this;for(var o=Object.keys(t).sort(),i=[t,r],s=Array.isArray(t)?this._arrayTreeMap:this._objectTreeMap,c=0;c<o.length;c++){var u=o[c];s.has(u)||s.set(u,new e),s=s.get(u);var a=t[u];s.has(a)||s.set(a,new e),s=s.get(a)}var l=s.get("_ekm_value");return l&&this._map.delete(l[0]),s.set("_ekm_value",i),this._map.set(t,i),this}},{key:"get",value:function(e){if(null===e||"object"!==n(e))return this._map.get(e);var t=i(this,e);return t?t[1]:void 0}},{key:"has",value:function(e){return null===e||"object"!==n(e)?this._map.has(e):void 0!==i(this,e)}},{key:"delete",value:function(e){return!!this.has(e)&&(this.set(e,void 0),!0)}},{key:"forEach",value:function(e){var t=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this;this._map.forEach((function(o,i){null!==i&&"object"===n(i)&&(o=o[1]),e.call(r,o,i,t)}))}},{key:"clear",value:function(){this._map=new Map,this._arrayTreeMap=new Map,this._objectTreeMap=new Map}},{key:"size",get:function(){return this._map.size}}])&&o(t.prototype,r),e}();e.exports=s},28:function(e,t){e.exports=function(e){var t,r=Object.keys(e);return t=function(){var e,t,n;for(e="return {",t=0;t<r.length;t++)e+=(n=JSON.stringify(r[t]))+":r["+n+"](s["+n+"],a),";return e+="}",new Function("r,s,a",e)}(),function(n,o){var i,s,c;if(void 0===n)return t(e,{},o);for(i=t(e,n,o),s=r.length;s--;)if(n[c=r[s]]!==i[c])return i;return n}}},40:function(e,t){e.exports=window.wp.reduxRoutine},41:function(e,t){function r(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}e.exports=r,e.exports.default=r},5:function(e,t){e.exports=function(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e},e.exports.default=e.exports,e.exports.__esModule=!0},6:function(e,t){e.exports=window.lodash},76:function(e,t,r){"use strict";r.r(t);var n={};r.r(n),r.d(n,"getIsResolving",(function(){return x})),r.d(n,"hasStartedResolution",(function(){return U})),r.d(n,"hasFinishedResolution",(function(){return k})),r.d(n,"isResolving",(function(){return F})),r.d(n,"getCachedResolvers",(function(){return D}));var o={};r.r(o),r.d(o,"startResolution",(function(){return V})),r.d(o,"finishResolution",(function(){return M})),r.d(o,"startResolutions",(function(){return C})),r.d(o,"finishResolutions",(function(){return G})),r.d(o,"invalidateResolution",(function(){return H})),r.d(o,"invalidateResolutionForStore",(function(){return K})),r.d(o,"invalidateResolutionForStoreSelector",(function(){return X}));var i=r(5),s=r.n(i),c=r(17);function u(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function a(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?u(Object(r),!0).forEach((function(t){Object(c.a)(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):u(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}function l(e){return"Minified Redux error #"+e+"; visit https://redux.js.org/Errors?code="+e+" for the full message or use the non-minified dev environment for full errors. "}var f="function"==typeof Symbol&&Symbol.observable||"@@observable",p=function(){return Math.random().toString(36).substring(7).split("").join(".")},d={INIT:"@@redux/INIT"+p(),REPLACE:"@@redux/REPLACE"+p(),PROBE_UNKNOWN_ACTION:function(){return"@@redux/PROBE_UNKNOWN_ACTION"+p()}};function b(e){if("object"!=typeof e||null===e)return!1;for(var t=e;null!==Object.getPrototypeOf(t);)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}function O(e,t,r){var n;if("function"==typeof t&&"function"==typeof r||"function"==typeof r&&"function"==typeof arguments[3])throw new Error(l(0));if("function"==typeof t&&void 0===r&&(r=t,t=void 0),void 0!==r){if("function"!=typeof r)throw new Error(l(1));return r(O)(e,t)}if("function"!=typeof e)throw new Error(l(2));var o=e,i=t,s=[],c=s,u=!1;function a(){c===s&&(c=s.slice())}function p(){if(u)throw new Error(l(3));return i}function y(e){if("function"!=typeof e)throw new Error(l(4));if(u)throw new Error(l(5));var t=!0;return a(),c.push(e),function(){if(t){if(u)throw new Error(l(6));t=!1,a();var r=c.indexOf(e);c.splice(r,1),s=null}}}function h(e){if(!b(e))throw new Error(l(7));if(void 0===e.type)throw new Error(l(8));if(u)throw new Error(l(9));try{u=!0,i=o(i,e)}finally{u=!1}for(var t=s=c,r=0;r<t.length;r++)(0,t[r])();return e}function g(e){if("function"!=typeof e)throw new Error(l(10));o=e,h({type:d.REPLACE})}function v(){var e,t=y;return(e={subscribe:function(e){if("object"!=typeof e||null===e)throw new Error(l(11));function r(){e.next&&e.next(p())}return r(),{unsubscribe:t(r)}}})[f]=function(){return this},e}return h({type:d.INIT}),(n={dispatch:h,subscribe:y,getState:p,replaceReducer:g})[f]=v,n}function y(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return 0===t.length?function(e){return e}:1===t.length?t[0]:t.reduce((function(e,t){return function(){return e(t.apply(void 0,arguments))}}))}function h(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return function(e){return function(){var r=e.apply(void 0,arguments),n=function(){throw new Error(l(15))},o={getState:r.getState,dispatch:function(){return n.apply(void 0,arguments)}},i=t.map((function(e){return e(o)}));return n=y.apply(void 0,i)(r.dispatch),a(a({},r),{},{dispatch:n})}}}var g=r(6),v=r(28),S=r.n(v),_=r(22),w=r.n(_),R=r(40),m=r.n(R);function E(e){return e.isRegistryControl=!0,e}const j={"@@data/SELECT":E(e=>({storeKey:t,selectorName:r,args:n})=>e.select(t)[r](...n)),"@@data/RESOLVE_SELECT":E(e=>({storeKey:t,selectorName:r,args:n})=>{const o=e.select(t)[r].hasResolver?"resolveSelect":"select";return e[o](t)[r](...n)}),"@@data/DISPATCH":E(e=>({storeKey:t,actionName:r,args:n})=>e.dispatch(t)[r](...n))};var T=r(41),I=r.n(T),N=()=>e=>t=>I()(t)?t.then(t=>{if(t)return e(t)}):e(t),A=(e,t)=>()=>r=>n=>{const o=e.select("core/data").getCachedResolvers(t);return Object.entries(o).forEach(([r,o])=>{const i=Object(g.get)(e.stores,[t,"resolvers",r]);i&&i.shouldInvalidate&&o.forEach((o,s)=>{!1===o&&i.shouldInvalidate(n,...s)&&e.dispatch("core/data").invalidateResolution(t,r,s)})}),r(n)};const L=("selectorName",e=>(t={},r)=>{const n=r.selectorName;if(void 0===n)return t;const o=e(t[n],r);return o===t[n]?t:{...t,[n]:o}})((e=new w.a,t)=>{switch(t.type){case"START_RESOLUTION":case"FINISH_RESOLUTION":{const r="START_RESOLUTION"===t.type,n=new w.a(e);return n.set(t.args,r),n}case"START_RESOLUTIONS":case"FINISH_RESOLUTIONS":{const r="START_RESOLUTIONS"===t.type,n=new w.a(e);for(const e of t.args)n.set(e,r);return n}case"INVALIDATE_RESOLUTION":{const r=new w.a(e);return r.delete(t.args),r}}return e});var P=(e={},t)=>{switch(t.type){case"INVALIDATE_RESOLUTION_FOR_STORE":return{};case"INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR":return Object(g.has)(e,[t.selectorName])?Object(g.omit)(e,[t.selectorName]):e;case"START_RESOLUTION":case"FINISH_RESOLUTION":case"START_RESOLUTIONS":case"FINISH_RESOLUTIONS":case"INVALIDATE_RESOLUTION":return L(e,t)}return e};function x(e,t,r){const n=Object(g.get)(e,[t]);if(n)return n.get(r)}function U(e,t,r=[]){return void 0!==x(e,t,r)}function k(e,t,r=[]){return!1===x(e,t,r)}function F(e,t,r=[]){return!0===x(e,t,r)}function D(e){return e}function V(e,t){return{type:"START_RESOLUTION",selectorName:e,args:t}}function M(e,t){return{type:"FINISH_RESOLUTION",selectorName:e,args:t}}function C(e,t){return{type:"START_RESOLUTIONS",selectorName:e,args:t}}function G(e,t){return{type:"FINISH_RESOLUTIONS",selectorName:e,args:t}}function H(e,t){return{type:"INVALIDATE_RESOLUTION",selectorName:e,args:t}}function K(){return{type:"INVALIDATE_RESOLUTION_FOR_STORE"}}function X(e){return{type:"INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR",selectorName:e}}function z(e,t){return{name:e,instantiate:r=>{const i=t.reducer,s=function(e,t,r,n){const o={...t.controls,...j},i=Object(g.mapValues)(o,e=>e.isRegistryControl?e(r):e),s=[A(r,e),N,m()(i)];var c;t.__experimentalUseThunks&&s.push((c=n,()=>e=>t=>"function"==typeof t?t(c):e(t)));const u=[h(...s)];"undefined"!=typeof window&&window.__REDUX_DEVTOOLS_EXTENSION__&&u.push(window.__REDUX_DEVTOOLS_EXTENSION__({name:e,instanceId:e}));const{reducer:a,initialState:l}=t;return O(S()({metadata:P,root:a}),{root:l},Object(g.flowRight)(u))}(e,t,r,{registry:r,get dispatch(){return Object.assign(e=>s.dispatch(e),d())},get select(){return Object.assign(e=>e(s.__unstableOriginalGetState()),p())},get resolveSelect(){return b()}}),c=function(){const e={};return{isRunning:(t,r)=>e[t]&&e[t].get(r),clear(t,r){e[t]&&e[t].delete(r)},markAsRunning(t,r){e[t]||(e[t]=new w.a),e[t].set(r,!0)}}}();let u;const a=function(e,t){return Object(g.mapValues)(e,e=>(...r)=>Promise.resolve(t.dispatch(e(...r))))}({...o,...t.actions},s);let l=function(e,t){return Object(g.mapValues)(e,e=>{const r=function(){const r=arguments.length,n=new Array(r+1);n[0]=t.__unstableOriginalGetState();for(let e=0;e<r;e++)n[e+1]=arguments[e];return e(...n)};return r.hasResolver=!1,r})}({...Object(g.mapValues)(n,e=>(t,...r)=>e(t.metadata,...r)),...Object(g.mapValues)(t.selectors,e=>(e.isRegistrySelector&&(e.registry=r),(t,...r)=>e(t.root,...r)))},s);if(t.resolvers){const e=function(e,t,r,n){const o=Object(g.mapValues)(e,e=>e.fulfill?e:{...e,fulfill:e});return{resolvers:o,selectors:Object(g.mapValues)(t,(t,i)=>{const s=e[i];if(!s)return t.hasResolver=!1,t;const c=(...e)=>(async function(){const t=r.getState();if(n.isRunning(i,e)||"function"==typeof s.isFulfilled&&s.isFulfilled(t,...e))return;const{metadata:c}=r.__unstableOriginalGetState();U(c,i,e)||(n.markAsRunning(i,e),setTimeout(async()=>{n.clear(i,e),r.dispatch(V(i,e)),await async function(e,t,r,...n){const o=Object(g.get)(t,[r]);if(!o)return;const i=o.fulfill(...n);i&&await e.dispatch(i)}(r,o,i,...e),r.dispatch(M(i,e))}))}(...e),t(...e));return c.hasResolver=!0,c})}}(t.resolvers,l,s,c);u=e.resolvers,l=e.selectors}const f=function(e,t){return Object(g.mapValues)(Object(g.omit)(e,["getIsResolving","hasStartedResolution","hasFinishedResolution","isResolving","getCachedResolvers"]),(r,n)=>(...o)=>new Promise(i=>{const s=()=>e.hasFinishedResolution(n,o),c=()=>r.apply(null,o),u=c();if(s())return i(u);const a=t.subscribe(()=>{s()&&(a(),i(c()))})}))}(l,s),p=()=>l,d=()=>a,b=()=>f;s.__unstableOriginalGetState=s.getState,s.getState=()=>s.__unstableOriginalGetState().root;const y=s&&(e=>{let t=s.__unstableOriginalGetState();return s.subscribe(()=>{const r=s.__unstableOriginalGetState(),n=r!==t;t=r,n&&e()})});return{reducer:i,store:s,actions:a,selectors:l,resolvers:u,getSelectors:p,getResolveSelectors:b,getActions:d,subscribe:y}}}}var B=function(e={},t=null){const r={};let n=[];const o=new Set;function i(){n.forEach(e=>e())}const s=e=>(n.push(e),()=>{n=Object(g.without)(n,e)});function c(e,t){if("function"!=typeof t.getSelectors)throw new TypeError("config.getSelectors must be a function");if("function"!=typeof t.getActions)throw new TypeError("config.getActions must be a function");if("function"!=typeof t.subscribe)throw new TypeError("config.subscribe must be a function");r[e]=t,t.subscribe(i)}let u={registerGenericStore:c,stores:r,namespaces:r,subscribe:s,select:function(e){const n=Object(g.isObject)(e)?e.name:e;o.add(n);const i=r[n];return i?i.getSelectors():t&&t.select(n)},resolveSelect:function(e){const n=Object(g.isObject)(e)?e.name:e;o.add(n);const i=r[n];return i?i.getResolveSelectors():t&&t.resolveSelect(n)},dispatch:function(e){const n=Object(g.isObject)(e)?e.name:e,o=r[n];return o?o.getActions():t&&t.dispatch(n)},use:function(e,t){return u={...u,...e(u,t)},u},register:function(e){c(e.name,e.instantiate(u))},__experimentalMarkListeningStores:function(e,t){o.clear();const r=e.call(this);return t.current=Array.from(o),r},__experimentalSubscribeStore:function(e,n){return e in r?r[e].subscribe(n):t?t.__experimentalSubscribeStore(e,n):s(n)},registerStore:(e,t)=>{if(!t.reducer)throw new TypeError("Must specify store reducer");const r=z(e,t).instantiate(u);return c(e,r),r.store}};return c("core/data",function(e){const t=t=>(r,...n)=>e.select(r)[t](...n),r=t=>(r,...n)=>e.dispatch(r)[t](...n);return{getSelectors:()=>["getIsResolving","hasStartedResolution","hasFinishedResolution","isResolving","getCachedResolvers"].reduce((e,r)=>({...e,[r]:t(r)}),{}),getActions:()=>["startResolution","finishResolution","invalidateResolution","invalidateResolutionForStore","invalidateResolutionForStoreSelector"].reduce((e,t)=>({...e,[t]:r(t)}),{}),subscribe:()=>()=>{}}}(u)),Object.entries(e).forEach(([e,t])=>u.registerStore(e,t)),t&&t.subscribe(i),a=u,Object(g.mapValues)(a,(e,t)=>"function"!=typeof e?e:function(){return u[t].apply(null,arguments)});var a}();B.select,B.resolveSelect,B.dispatch,B.subscribe,B.registerGenericStore,B.registerStore,B.use;const W=B.register;function J(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function q(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?J(Object(r),!0).forEach((function(t){s()(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):J(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}window.wp.blockEditor.store="core/block-editor",window.wp.editor.store="core/editor",window.wp.notices.store="core/notices",window.wp.data=q(q({},window.wp.data),{},{createReduxStore:z,register:W})}}); \ No newline at end of file diff --git a/libraries/lifterlms-blocks/assets/js/llms-blocks.asset.php b/libraries/lifterlms-blocks/assets/js/llms-blocks.asset.php new file mode 100644 index 0000000000..032f248866 --- /dev/null +++ b/libraries/lifterlms-blocks/assets/js/llms-blocks.asset.php @@ -0,0 +1 @@ +<?php return array('dependencies' => array('jquery', 'lodash', 'react', 'react-dom', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-plugins', 'wp-polyfill', 'wp-rich-text', 'wp-server-side-render', 'wp-url'), 'version' => '59f0d96c303b2be3cb16b1f516d10f2f'); \ No newline at end of file diff --git a/libraries/lifterlms-blocks/assets/js/llms-blocks.js b/libraries/lifterlms-blocks/assets/js/llms-blocks.js new file mode 100644 index 0000000000..84111e5a4a --- /dev/null +++ b/libraries/lifterlms-blocks/assets/js/llms-blocks.js @@ -0,0 +1,24 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=75)}([function(e,t){e.exports=window.wp.element},function(e,t){e.exports=window.wp.i18n},function(e,t){e.exports=window.React},function(e,t){e.exports=window.wp.components},function(e,t){e.exports=window.wp.data},function(e,t){e.exports=function(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.lodash},function(e,t){e.exports=window.wp.blockEditor},function(e,t){function n(t){return e.exports=n=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},e.exports.default=e.exports,e.exports.__esModule=!0,n(t)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.wp.blocks},function(e,t){e.exports=window.wp.hooks},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(48);e.exports=function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&r(e,t)},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(31).default,o=n(9);e.exports=function(e,t){return!t||"object"!==r(t)&&"function"!=typeof t?o(e):t},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}e.exports=function(e,t,r){return t&&n(e.prototype,t),r&&n(e,r),e},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.wp.compose},function(e,t,n){"use strict";function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}n.d(t,"a",(function(){return r}))},function(e,t){e.exports=window.wp.editor},function(e,t,n){var r=n(69),o=n(70),i=n(33),l=n(71);e.exports=function(e,t){return r(e)||o(e,t)||i(e,t)||l()},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.wp.serverSideRender},function(e,t){e.exports=window.wp.editPost},,function(e,t){e.exports=window.wp.richText},function(e,t){e.exports=window.ReactDOM},function(e,t){function n(){return e.exports=n=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},e.exports.default=e.exports,e.exports.__esModule=!0,n.apply(this,arguments)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.jQuery},function(e,t){e.exports=window.wp.plugins},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},o=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),i=n(2),l=a(i),s=a(n(51));function a(e){return e&&e.__esModule?e:{default:e}}var c={position:"absolute",top:0,left:0,visibility:"hidden",height:0,overflow:"scroll",whiteSpace:"pre"},u=["extraWidth","injectStyles","inputClassName","inputRef","inputStyle","minWidth","onAutosize","placeholderIsMinWidth"],f=function(e,t){t.style.fontSize=e.fontSize,t.style.fontFamily=e.fontFamily,t.style.fontWeight=e.fontWeight,t.style.fontStyle=e.fontStyle,t.style.letterSpacing=e.letterSpacing,t.style.textTransform=e.textTransform},d=!("undefined"==typeof window||!window.navigator)&&/MSIE |Trident\/|Edge\//.test(window.navigator.userAgent),p=function(){return d?"_"+Math.random().toString(36).substr(2,12):void 0},m=function(e){function t(e){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t);var n=function(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return n.inputRef=function(e){n.input=e,"function"==typeof n.props.inputRef&&n.props.inputRef(e)},n.placeHolderSizerRef=function(e){n.placeHolderSizer=e},n.sizerRef=function(e){n.sizer=e},n.state={inputWidth:e.minWidth,inputId:e.id||p(),prevId:e.id},n}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}(t,e),o(t,null,[{key:"getDerivedStateFromProps",value:function(e,t){var n=e.id;return n!==t.prevId?{inputId:n||p(),prevId:n}:null}}]),o(t,[{key:"componentDidMount",value:function(){this.mounted=!0,this.copyInputStyles(),this.updateInputWidth()}},{key:"componentDidUpdate",value:function(e,t){t.inputWidth!==this.state.inputWidth&&"function"==typeof this.props.onAutosize&&this.props.onAutosize(this.state.inputWidth),this.updateInputWidth()}},{key:"componentWillUnmount",value:function(){this.mounted=!1}},{key:"copyInputStyles",value:function(){if(this.mounted&&window.getComputedStyle){var e=this.input&&window.getComputedStyle(this.input);e&&(f(e,this.sizer),this.placeHolderSizer&&f(e,this.placeHolderSizer))}}},{key:"updateInputWidth",value:function(){if(this.mounted&&this.sizer&&void 0!==this.sizer.scrollWidth){var e=void 0;e=this.props.placeholder&&(!this.props.value||this.props.value&&this.props.placeholderIsMinWidth)?Math.max(this.sizer.scrollWidth,this.placeHolderSizer.scrollWidth)+2:this.sizer.scrollWidth+2,(e+="number"===this.props.type&&void 0===this.props.extraWidth?16:parseInt(this.props.extraWidth)||0)<this.props.minWidth&&(e=this.props.minWidth),e!==this.state.inputWidth&&this.setState({inputWidth:e})}}},{key:"getInput",value:function(){return this.input}},{key:"focus",value:function(){this.input.focus()}},{key:"blur",value:function(){this.input.blur()}},{key:"select",value:function(){this.input.select()}},{key:"renderStyles",value:function(){var e=this.props.injectStyles;return d&&e?l.default.createElement("style",{dangerouslySetInnerHTML:{__html:"input#"+this.state.inputId+"::-ms-clear {display: none;}"}}):null}},{key:"render",value:function(){var e=[this.props.defaultValue,this.props.value,""].reduce((function(e,t){return null!=e?e:t})),t=r({},this.props.style);t.display||(t.display="inline-block");var n=r({boxSizing:"content-box",width:this.state.inputWidth+"px"},this.props.inputStyle),o=function(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}(this.props,[]);return function(e){u.forEach((function(t){return delete e[t]}))}(o),o.className=this.props.inputClassName,o.id=this.state.inputId,o.style=n,l.default.createElement("div",{className:this.props.className,style:t},this.renderStyles(),l.default.createElement("input",r({},o,{ref:this.inputRef})),l.default.createElement("div",{ref:this.sizerRef,style:c},e),this.props.placeholder?l.default.createElement("div",{ref:this.placeHolderSizerRef,style:c},this.props.placeholder):null)}}]),t}(i.Component);m.propTypes={className:s.default.string,defaultValue:s.default.any,extraWidth:s.default.oneOfType([s.default.number,s.default.string]),id:s.default.string,injectStyles:s.default.bool,inputClassName:s.default.string,inputRef:s.default.func,inputStyle:s.default.object,minWidth:s.default.oneOfType([s.default.number,s.default.string]),onAutosize:s.default.func,onChange:s.default.func,placeholder:s.default.string,placeholderIsMinWidth:s.default.bool,style:s.default.object,value:s.default.any},m.defaultProps={minWidth:1,injectStyles:!0},t.default=m},function(e,t,n){"use strict";var r=n(58),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},i={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},l={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function a(e){return r.isMemo(e)?l:s[e.$$typeof]||o}s[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[r.Memo]=l;var c=Object.defineProperty,u=Object.getOwnPropertyNames,f=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(m){var o=p(n);o&&o!==m&&e(t,o,r)}var l=u(n);f&&(l=l.concat(f(n)));for(var s=a(t),b=a(n),h=0;h<l.length;++h){var v=l[h];if(!(i[v]||r&&r[v]||b&&b[v]||s&&s[v])){var g=d(n,v);try{c(t,v,g)}catch(e){}}}}return t}},function(e,t){function n(t){return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?(e.exports=n=function(e){return typeof e},e.exports.default=e.exports,e.exports.__esModule=!0):(e.exports=n=function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.default=e.exports,e.exports.__esModule=!0),n(t)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(32);e.exports=function(e,t){if(e){if("string"==typeof e)return r(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(e,t):void 0}},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){!function(e){"use strict";function t(e,t,n,r){var o,i=!1,l=0;function s(){o&&clearTimeout(o)}function a(){for(var a=arguments.length,c=new Array(a),u=0;u<a;u++)c[u]=arguments[u];var f=this,d=Date.now()-l;function p(){l=Date.now(),n.apply(f,c)}function m(){o=void 0}i||(r&&!o&&p(),s(),void 0===r&&d>e?p():!0!==t&&(o=setTimeout(r?m:p,void 0===r?e-d:e)))}return"boolean"!=typeof t&&(r=n,n=t,t=void 0),a.cancel=function(){s(),i=!0},a}e.debounce=function(e,n,r){return void 0===r?t(e,n,!1):t(e,r,!1!==n)},e.throttle=t,Object.defineProperty(e,"__esModule",{value:!0})}(t)},function(e,t){e.exports=window.wp.domReady},function(e,t,n){var r=n(68);e.exports=function(e,t){if(null==e)return{};var n,o,i=r(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(o=0;o<l.length;o++)n=l[o],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(72);function o(t,n,i){return"undefined"!=typeof Reflect&&Reflect.get?(e.exports=o=Reflect.get,e.exports.default=e.exports,e.exports.__esModule=!0):(e.exports=o=function(e,t,n){var o=r(e,t);if(o){var i=Object.getOwnPropertyDescriptor(o,t);return i.get?i.get.call(n):i.value}},e.exports.default=e.exports,e.exports.__esModule=!0),o(t,n,i||t)}e.exports=o,e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=window.wp.url},function(e,t){e.exports=window.wp.notices},,,,,,,,function(e,t,n){},function(e,t){function n(t,r){return e.exports=n=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},e.exports.default=e.exports,e.exports.__esModule=!0,n(t,r)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){},function(e,t){e.exports=function(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){e.exports=n(52)()},function(e,t,n){"use strict";var r=n(53);function o(){}function i(){}i.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,i,l){if(l!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:i,resetWarningCache:o};return n.PropTypes=n,n}},function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,n){var r=n(55),o=n(56),i=n(33),l=n(57);e.exports=function(e){return r(e)||o(e)||i(e)||l()},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(32);e.exports=function(e){if(Array.isArray(e))return r(e)},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){"use strict";e.exports=n(59)},function(e,t,n){"use strict";var r="function"==typeof Symbol&&Symbol.for,o=r?Symbol.for("react.element"):60103,i=r?Symbol.for("react.portal"):60106,l=r?Symbol.for("react.fragment"):60107,s=r?Symbol.for("react.strict_mode"):60108,a=r?Symbol.for("react.profiler"):60114,c=r?Symbol.for("react.provider"):60109,u=r?Symbol.for("react.context"):60110,f=r?Symbol.for("react.async_mode"):60111,d=r?Symbol.for("react.concurrent_mode"):60111,p=r?Symbol.for("react.forward_ref"):60112,m=r?Symbol.for("react.suspense"):60113,b=r?Symbol.for("react.suspense_list"):60120,h=r?Symbol.for("react.memo"):60115,v=r?Symbol.for("react.lazy"):60116,g=r?Symbol.for("react.block"):60121,y=r?Symbol.for("react.fundamental"):60117,O=r?Symbol.for("react.responder"):60118,_=r?Symbol.for("react.scope"):60119;function j(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case o:switch(e=e.type){case f:case d:case l:case a:case s:case m:return e;default:switch(e=e&&e.$$typeof){case u:case p:case v:case h:case c:return e;default:return t}}case i:return t}}}function w(e){return j(e)===d}t.AsyncMode=f,t.ConcurrentMode=d,t.ContextConsumer=u,t.ContextProvider=c,t.Element=o,t.ForwardRef=p,t.Fragment=l,t.Lazy=v,t.Memo=h,t.Portal=i,t.Profiler=a,t.StrictMode=s,t.Suspense=m,t.isAsyncMode=function(e){return w(e)||j(e)===f},t.isConcurrentMode=w,t.isContextConsumer=function(e){return j(e)===u},t.isContextProvider=function(e){return j(e)===c},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===o},t.isForwardRef=function(e){return j(e)===p},t.isFragment=function(e){return j(e)===l},t.isLazy=function(e){return j(e)===v},t.isMemo=function(e){return j(e)===h},t.isPortal=function(e){return j(e)===i},t.isProfiler=function(e){return j(e)===a},t.isStrictMode=function(e){return j(e)===s},t.isSuspense=function(e){return j(e)===m},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===l||e===d||e===a||e===s||e===m||e===b||"object"==typeof e&&null!==e&&(e.$$typeof===v||e.$$typeof===h||e.$$typeof===c||e.$$typeof===u||e.$$typeof===p||e.$$typeof===y||e.$$typeof===O||e.$$typeof===_||e.$$typeof===g)},t.typeOf=j},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t){e.exports=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r<i.length;r++)n=i[r],t.indexOf(n)>=0||(o[n]=e[n]);return o},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(e){if(Array.isArray(e))return e},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(e,t){var n=e&&("undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"]);if(null!=n){var r,o,i=[],l=!0,s=!1;try{for(n=n.call(e);!(l=(r=n.next()).done)&&(i.push(r.value),!t||i.length!==t);l=!0);}catch(e){s=!0,o=e}finally{try{l||null==n.return||n.return()}finally{if(s)throw o}}return i}},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t){e.exports=function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){var r=n(8);e.exports=function(e,t){for(;!Object.prototype.hasOwnProperty.call(e,t)&&null!==(e=r(e)););return e},e.exports.default=e.exports,e.exports.__esModule=!0},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";n.r(t);var r={};n.r(r),n.d(r,"addField",(function(){return vo})),n.d(r,"deleteField",(function(){return go})),n.d(r,"editField",(function(){return yo})),n.d(r,"loadField",(function(){return Oo})),n.d(r,"unloadField",(function(){return _o})),n.d(r,"receiveFields",(function(){return jo})),n.d(r,"renameField",(function(){return wo})),n.d(r,"resetFields",(function(){return xo}));var o={};n.r(o),n.d(o,"fieldExists",(function(){return Eo})),n.d(o,"getField",(function(){return ko})),n.d(o,"getFieldBy",(function(){return Co})),n.d(o,"getFields",(function(){return So})),n.d(o,"getLoadedFields",(function(){return Po})),n.d(o,"isDuplicate",(function(){return Io})),n.d(o,"isLoaded",(function(){return Do}));var i={};n.r(i),n.d(i,"name",(function(){return Vo})),n.d(i,"postTypes",(function(){return Uo})),n.d(i,"settings",(function(){return Ho}));var l={};n.r(l),n.d(l,"name",(function(){return $o})),n.d(l,"postTypes",(function(){return Wo})),n.d(l,"settings",(function(){return Go}));var s={};n.r(s),n.d(s,"postTypes",(function(){return Ko})),n.d(s,"name",(function(){return Yo})),n.d(s,"settings",(function(){return Xo}));var a={};n.r(a),n.d(a,"name",(function(){return Zo})),n.d(a,"postTypes",(function(){return ei})),n.d(a,"settings",(function(){return ti}));var c={};n.r(c),n.d(c,"name",(function(){return oi})),n.d(c,"postTypes",(function(){return ii})),n.d(c,"settings",(function(){return li}));var u={};n.r(u),n.d(u,"name",(function(){return si})),n.d(u,"postTypes",(function(){return ai})),n.d(u,"settings",(function(){return ci}));var f={};n.r(f),n.d(f,"name",(function(){return ui})),n.d(f,"postTypes",(function(){return fi})),n.d(f,"settings",(function(){return di}));var d={};n.r(d),n.d(d,"name",(function(){return hi})),n.d(d,"postTypes",(function(){return vi})),n.d(d,"settings",(function(){return gi}));var p={};n.r(p),n.d(p,"name",(function(){return yi})),n.d(p,"settings",(function(){return Oi}));var m={};n.r(m),n.d(m,"Search",(function(){return $r})),n.d(m,"SearchPost",(function(){return Wr})),n.d(m,"SearchUser",(function(){return Ui})),n.d(m,"SortableList",(function(){return Ns})),n.d(m,"SortableDragHandle",(function(){return Ls}));var b={};n.r(b),n.d(b,"name",(function(){return aa})),n.d(b,"postTypes",(function(){return ca})),n.d(b,"composed",(function(){return ua})),n.d(b,"settings",(function(){return va}));var h={};n.r(h),n.d(h,"name",(function(){return _a})),n.d(h,"postTypes",(function(){return ja})),n.d(h,"composed",(function(){return wa})),n.d(h,"settings",(function(){return xa}));var v={};n.r(v),n.d(v,"name",(function(){return Sa})),n.d(v,"postTypes",(function(){return Pa})),n.d(v,"composed",(function(){return Ia})),n.d(v,"settings",(function(){return Da}));var g={};n.r(g),n.d(g,"name",(function(){return La})),n.d(g,"postTypes",(function(){return Aa})),n.d(g,"composed",(function(){return Na})),n.d(g,"settings",(function(){return Fa}));var y={};n.r(y),n.d(y,"name",(function(){return za})),n.d(y,"postTypes",(function(){return $a})),n.d(y,"composed",(function(){return Wa})),n.d(y,"settings",(function(){return Ka}));var O={};n.r(O),n.d(O,"name",(function(){return Qa})),n.d(O,"composed",(function(){return Ja})),n.d(O,"settings",(function(){return Za})),n.d(O,"postTypes",(function(){return $a}));var _={};n.r(_),n.d(_,"name",(function(){return ec})),n.d(_,"composed",(function(){return tc})),n.d(_,"settings",(function(){return nc})),n.d(_,"postTypes",(function(){return $a}));var j={};n.r(j),n.d(j,"name",(function(){return rc})),n.d(j,"composed",(function(){return oc})),n.d(j,"settings",(function(){return ic})),n.d(j,"postTypes",(function(){return $a}));var w={};n.r(w),n.d(w,"name",(function(){return lc})),n.d(w,"composed",(function(){return sc})),n.d(w,"settings",(function(){return ac})),n.d(w,"postTypes",(function(){return $a}));var x={};n.r(x),n.d(x,"name",(function(){return cc})),n.d(x,"composed",(function(){return uc})),n.d(x,"settings",(function(){return fc})),n.d(x,"postTypes",(function(){return $a}));var E={};n.r(E),n.d(E,"name",(function(){return dc})),n.d(E,"composed",(function(){return pc})),n.d(E,"settings",(function(){return mc})),n.d(E,"postTypes",(function(){return $a}));var k={};n.r(k),n.d(k,"name",(function(){return bc})),n.d(k,"composed",(function(){return hc})),n.d(k,"settings",(function(){return vc})),n.d(k,"postTypes",(function(){return $a}));var C={};n.r(C),n.d(C,"name",(function(){return gc})),n.d(C,"postTypes",(function(){return yc})),n.d(C,"composed",(function(){return Oc})),n.d(C,"settings",(function(){return _c}));var S={};n.r(S),n.d(S,"name",(function(){return xc})),n.d(S,"composed",(function(){return Ec})),n.d(S,"settings",(function(){return Cc})),n.d(S,"postTypes",(function(){return $a}));var P={};n.r(P),n.d(P,"name",(function(){return Sc})),n.d(P,"postTypes",(function(){return Pc})),n.d(P,"composed",(function(){return Ic})),n.d(P,"settings",(function(){return Dc}));var I={};n.r(I),n.d(I,"name",(function(){return Rc})),n.d(I,"postTypes",(function(){return Tc})),n.d(I,"composed",(function(){return Mc})),n.d(I,"settings",(function(){return Lc}));var D={};n.r(D),n.d(D,"name",(function(){return Ac})),n.d(D,"composed",(function(){return Nc})),n.d(D,"settings",(function(){return Fc})),n.d(D,"postTypes",(function(){return $a}));var R={};n.r(R),n.d(R,"name",(function(){return Bc})),n.d(R,"composed",(function(){return Vc})),n.d(R,"settings",(function(){return Uc})),n.d(R,"postTypes",(function(){return $a}));var T={};n.r(T),n.d(T,"name",(function(){return Hc})),n.d(T,"composed",(function(){return qc})),n.d(T,"settings",(function(){return zc})),n.d(T,"postTypes",(function(){return $a}));var M={};n.r(M),n.d(M,"name",(function(){return $c})),n.d(M,"composed",(function(){return Wc})),n.d(M,"settings",(function(){return Gc})),n.d(M,"postTypes",(function(){return Aa}));var L={};n.r(L),n.d(L,"name",(function(){return Kc})),n.d(L,"postTypes",(function(){return Yc})),n.d(L,"composed",(function(){return Xc})),n.d(L,"settings",(function(){return Qc}));var A={};n.r(A),n.d(A,"name",(function(){return Jc})),n.d(A,"composed",(function(){return Zc})),n.d(A,"settings",(function(){return eu})),n.d(A,"postTypes",(function(){return Aa}));var N={};n.r(N),n.d(N,"name",(function(){return tu})),n.d(N,"composed",(function(){return nu})),n.d(N,"settings",(function(){return ru})),n.d(N,"postTypes",(function(){return $a}));var F={};n.r(F),n.d(F,"name",(function(){return ou})),n.d(F,"composed",(function(){return iu})),n.d(F,"settings",(function(){return lu})),n.d(F,"postTypes",(function(){return $a}));var B={};n.r(B),n.d(B,"confirmGroup",(function(){return b})),n.d(B,"checkboxes",(function(){return h})),n.d(B,"radio",(function(){return v})),n.d(B,"select",(function(){return g})),n.d(B,"text",(function(){return y})),n.d(B,"textarea",(function(){return O})),n.d(B,"redeemVoucher",(function(){return _})),n.d(B,"userAddress",(function(){return P})),n.d(B,"userAddressStreet",(function(){return I})),n.d(B,"userAddressStreetPrimary",(function(){return D})),n.d(B,"userAddressStreetSecondary",(function(){return R})),n.d(B,"userAddressCity",(function(){return T})),n.d(B,"userAddressCountry",(function(){return M})),n.d(B,"userAddressRegion",(function(){return L})),n.d(B,"userAddressState",(function(){return A})),n.d(B,"userAddressPostalCode",(function(){return N})),n.d(B,"userDisplayName",(function(){return j})),n.d(B,"userEmail",(function(){return x})),n.d(B,"userFirstName",(function(){return E})),n.d(B,"userLastName",(function(){return k})),n.d(B,"userLogin",(function(){return w})),n.d(B,"userNames",(function(){return C})),n.d(B,"userPassword",(function(){return S})),n.d(B,"userPhone",(function(){return F}));var V=n(5),U=n.n(V),H=(n(47),n(11));function q(e,t){var n=!0;return(-1!==window.llms.dynamic_blocks.indexOf(t)||e.supports&&!1===e.supports.llms_visibility||Object(H.applyFilters)("llms_block_visibility_disallowed_blocks",["core/freeform","llms/php-template"]).includes(t))&&(n=!1),Object(H.applyFilters)("llms_block_supports_visibility",n,e,t)}var z=n(0),$=n(1),W=n(16),G=n(7),K=n(3),Y=n(12),X=n.n(Y),Q=n(15),J=n.n(Q),Z=n(13),ee=n.n(Z),te=n(14),ne=n.n(te),re=n(8),oe=n.n(re),ie=(n(49),{all:Object($.__)("everyone","lifterlms"),enrolled:Object($.__)("enrolled users","lifterlms"),not_enrolled:Object($.__)("non-enrolled users or visitors","lifterlms"),logged_in:Object($.__)("logged in users","lifterlms"),logged_out:Object($.__)("logged out users","lifterlms")}),le=Object.keys(ie).map((function(e){return{label:ie[e],value:e}}));var se=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){return X()(this,o),r.apply(this,arguments)}return J()(o,[{key:"render",value:function(){var e,t=this.props.attributes.llms_visibility,n=this.props.children;return"all"===t?n:Object(z.createElement)("div",{className:"llms-block-visibility"},n,Object(z.createElement)("div",{className:"llms-block-visibility--indicator"},Object(z.createElement)(K.Dashicon,{icon:"visibility"}),Object(z.createElement)("span",{className:"llms-block-visibility--msg"},Object($.sprintf)(// Translators: %s = visibility setting label. +Object($.__)("This block is only visible to %s","lifterlms"),ie[e=t]||e))))}}]),o}(z.Component),ae=n(9),ce=n.n(ae),ue=n(34);function fe(){return(fe=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e}).apply(this,arguments)}function de(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r<i.length;r++)n=i[r],t.indexOf(n)>=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r<i.length;r++)n=i[r],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var pe=n(17),me=n(2),be=n.n(me),he=function(){function e(e){var t=this;this._insertTag=function(e){var n;n=0===t.tags.length?t.prepend?t.container.firstChild:t.before:t.tags[t.tags.length-1].nextSibling,t.container.insertBefore(e,n),t.tags.push(e)},this.isSpeedy=void 0===e.speedy||e.speedy,this.tags=[],this.ctr=0,this.nonce=e.nonce,this.key=e.key,this.container=e.container,this.prepend=e.prepend,this.before=null}var t=e.prototype;return t.hydrate=function(e){e.forEach(this._insertTag)},t.insert=function(e){this.ctr%(this.isSpeedy?65e3:1)==0&&this._insertTag(function(e){var t=document.createElement("style");return t.setAttribute("data-emotion",e.key),void 0!==e.nonce&&t.setAttribute("nonce",e.nonce),t.appendChild(document.createTextNode("")),t.setAttribute("data-s",""),t}(this));var t=this.tags[this.tags.length-1];if(this.isSpeedy){var n=function(e){if(e.sheet)return e.sheet;for(var t=0;t<document.styleSheets.length;t++)if(document.styleSheets[t].ownerNode===e)return document.styleSheets[t]}(t);try{n.insertRule(e,n.cssRules.length)}catch(e){}}else t.appendChild(document.createTextNode(e));this.ctr++},t.flush=function(){this.tags.forEach((function(e){return e.parentNode.removeChild(e)})),this.tags=[],this.ctr=0},e}(),ve="-ms-",ge="-moz-",ye="-webkit-",Oe="comm",_e="decl",je=Math.abs,we=String.fromCharCode;function xe(e){return e.trim()}function Ee(e,t,n){return e.replace(t,n)}function ke(e,t){return e.indexOf(t)}function Ce(e,t){return 0|e.charCodeAt(t)}function Se(e,t,n){return e.slice(t,n)}function Pe(e){return e.length}function Ie(e){return e.length}function De(e,t){return t.push(e),e}var Re=1,Te=1,Me=0,Le=0,Ae=0,Ne="";function Fe(e,t,n,r,o,i,l){return{value:e,root:t,parent:n,type:r,props:o,children:i,line:Re,column:Te,length:l,return:""}}function Be(e,t,n){return Fe(e,t.root,t.parent,n,t.props,t.children,0)}function Ve(){return Ae=Le>0?Ce(Ne,--Le):0,Te--,10===Ae&&(Te=1,Re--),Ae}function Ue(){return Ae=Le<Me?Ce(Ne,Le++):0,Te++,10===Ae&&(Te=1,Re++),Ae}function He(){return Ce(Ne,Le)}function qe(){return Le}function ze(e,t){return Se(Ne,e,t)}function $e(e){switch(e){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function We(e){return Re=Te=1,Me=Pe(Ne=e),Le=0,[]}function Ge(e){return Ne="",e}function Ke(e){return xe(ze(Le-1,function e(t){for(;Ue();)switch(Ae){case t:return Le;case 34:case 39:return e(34===t||39===t?t:Ae);case 40:41===t&&e(t);break;case 92:Ue()}return Le}(91===e?e+2:40===e?e+1:e)))}function Ye(e){for(;(Ae=He())&&Ae<33;)Ue();return $e(e)>2||$e(Ae)>3?"":" "}function Xe(e,t){for(;--t&&Ue()&&!(Ae<48||Ae>102||Ae>57&&Ae<65||Ae>70&&Ae<97););return ze(e,qe()+(t<6&&32==He()&&32==Ue()))}function Qe(e,t){for(;Ue()&&e+Ae!==57&&(e+Ae!==84||47!==He()););return"/*"+ze(t,Le-1)+"*"+we(47===e?e:Ue())}function Je(e){for(;!$e(He());)Ue();return ze(e,Le)}function Ze(e,t,n,r,o,i,l,s,a,c,u){for(var f=o-1,d=0===o?i:[""],p=Ie(d),m=0,b=0,h=0;m<r;++m)for(var v=0,g=Se(e,f+1,f=je(b=l[m])),y=e;v<p;++v)(y=xe(b>0?d[v]+" "+g:Ee(g,/&\f/g,d[v])))&&(a[h++]=y);return Fe(e,t,n,0===o?"rule":s,a,c,u)}function et(e,t,n){return Fe(e,t,n,Oe,we(Ae),Se(e,2,-2),0)}function tt(e,t,n,r){return Fe(e,t,n,_e,Se(e,0,r),Se(e,r+1,-1),r)}function nt(e,t){for(var n="",r=Ie(e),o=0;o<r;o++)n+=t(e[o],o,e,t)||"";return n}function rt(e,t,n,r){switch(e.type){case"@import":case _e:return e.return=e.return||e.value;case Oe:return"";case"rule":e.value=e.props.join(",")}return Pe(n=nt(e.children,r))?e.return=e.value+"{"+n+"}":""}function ot(e){return function(t){t.root||(t=t.return)&&e(t)}}var it=function(e){var t=Object.create(null);return function(n){return void 0===t[n]&&(t[n]=e(n)),t[n]}},lt=new WeakMap,st=function(e){if("rule"===e.type&&e.parent&&e.length){for(var t=e.value,n=e.parent,r=e.column===n.column&&e.line===n.line;"rule"!==n.type;)if(!(n=n.parent))return;if((1!==e.props.length||58===t.charCodeAt(0)||lt.get(n))&&!r){lt.set(e,!0);for(var o=[],i=function(e,t){return Ge(function(e,t){var n=-1,r=44;do{switch($e(r)){case 0:38===r&&12===He()&&(t[n]=1),e[n]+=Je(Le-1);break;case 2:e[n]+=Ke(r);break;case 4:if(44===r){e[++n]=58===He()?"&\f":"",t[n]=e[n].length;break}default:e[n]+=we(r)}}while(r=Ue());return e}(We(e),t))}(t,o),l=n.props,s=0,a=0;s<i.length;s++)for(var c=0;c<l.length;c++,a++)e.props[a]=o[s]?i[s].replace(/&\f/g,l[c]):l[c]+" "+i[s]}}},at=function(e){if("decl"===e.type){var t=e.value;108===t.charCodeAt(0)&&98===t.charCodeAt(2)&&(e.return="",e.value="")}},ct=[function(e,t,n,r){if(!e.return)switch(e.type){case _e:e.return=function e(t,n){switch(function(e,t){return(((t<<2^Ce(e,0))<<2^Ce(e,1))<<2^Ce(e,2))<<2^Ce(e,3)}(t,n)){case 5103:return ye+"print-"+t+t;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return ye+t+t;case 5349:case 4246:case 4810:case 6968:case 2756:return ye+t+ge+t+ve+t+t;case 6828:case 4268:return ye+t+ve+t+t;case 6165:return ye+t+ve+"flex-"+t+t;case 5187:return ye+t+Ee(t,/(\w+).+(:[^]+)/,ye+"box-$1$2"+ve+"flex-$1$2")+t;case 5443:return ye+t+ve+"flex-item-"+Ee(t,/flex-|-self/,"")+t;case 4675:return ye+t+ve+"flex-line-pack"+Ee(t,/align-content|flex-|-self/,"")+t;case 5548:return ye+t+ve+Ee(t,"shrink","negative")+t;case 5292:return ye+t+ve+Ee(t,"basis","preferred-size")+t;case 6060:return ye+"box-"+Ee(t,"-grow","")+ye+t+ve+Ee(t,"grow","positive")+t;case 4554:return ye+Ee(t,/([^-])(transform)/g,"$1"+ye+"$2")+t;case 6187:return Ee(Ee(Ee(t,/(zoom-|grab)/,ye+"$1"),/(image-set)/,ye+"$1"),t,"")+t;case 5495:case 3959:return Ee(t,/(image-set\([^]*)/,ye+"$1$`$1");case 4968:return Ee(Ee(t,/(.+:)(flex-)?(.*)/,ye+"box-pack:$3"+ve+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+ye+t+t;case 4095:case 3583:case 4068:case 2532:return Ee(t,/(.+)-inline(.+)/,ye+"$1$2")+t;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(Pe(t)-1-n>6)switch(Ce(t,n+1)){case 109:if(45!==Ce(t,n+4))break;case 102:return Ee(t,/(.+:)(.+)-([^]+)/,"$1"+ye+"$2-$3$1"+ge+(108==Ce(t,n+3)?"$3":"$2-$3"))+t;case 115:return~ke(t,"stretch")?e(Ee(t,"stretch","fill-available"),n)+t:t}break;case 4949:if(115!==Ce(t,n+1))break;case 6444:switch(Ce(t,Pe(t)-3-(~ke(t,"!important")&&10))){case 107:return Ee(t,":",":"+ye)+t;case 101:return Ee(t,/(.+:)([^;!]+)(;|!.+)?/,"$1"+ye+(45===Ce(t,14)?"inline-":"")+"box$3$1"+ye+"$2$3$1"+ve+"$2box$3")+t}break;case 5936:switch(Ce(t,n+11)){case 114:return ye+t+ve+Ee(t,/[svh]\w+-[tblr]{2}/,"tb")+t;case 108:return ye+t+ve+Ee(t,/[svh]\w+-[tblr]{2}/,"tb-rl")+t;case 45:return ye+t+ve+Ee(t,/[svh]\w+-[tblr]{2}/,"lr")+t}return ye+t+ve+t+t}return t}(e.value,e.length);break;case"@keyframes":return nt([Be(Ee(e.value,"@","@"+ye),e,"")],r);case"rule":if(e.length)return function(e,t){return e.map(t).join("")}(e.props,(function(t){switch(function(e,t){return(e=/(::plac\w+|:read-\w+)/.exec(e))?e[0]:e}(t)){case":read-only":case":read-write":return nt([Be(Ee(t,/:(read-\w+)/,":-moz-$1"),e,"")],r);case"::placeholder":return nt([Be(Ee(t,/:(plac\w+)/,":"+ye+"input-$1"),e,""),Be(Ee(t,/:(plac\w+)/,":-moz-$1"),e,""),Be(Ee(t,/:(plac\w+)/,ve+"input-$1"),e,"")],r)}return""}))}}],ut=function(e){var t=e.key;if("css"===t){var n=document.querySelectorAll("style[data-emotion]:not([data-s])");Array.prototype.forEach.call(n,(function(e){document.head.appendChild(e),e.setAttribute("data-s","")}))}var r,o,i=e.stylisPlugins||ct,l={},s=[];r=e.container||document.head,Array.prototype.forEach.call(document.querySelectorAll("style[data-emotion]"),(function(e){var n=e.getAttribute("data-emotion").split(" ");if(n[0]===t){for(var r=1;r<n.length;r++)l[n[r]]=!0;s.push(e)}}));var a,c=[st,at],u=[rt,ot((function(e){a.insert(e)}))],f=function(e){var t=Ie(e);return function(n,r,o,i){for(var l="",s=0;s<t;s++)l+=e[s](n,r,o,i)||"";return l}}(c.concat(i,u));o=function(e,t,n,r){a=n,nt(function(e){return Ge(function e(t,n,r,o,i,l,s,a,c){for(var u=0,f=0,d=s,p=0,m=0,b=0,h=1,v=1,g=1,y=0,O="",_=i,j=l,w=o,x=O;v;)switch(b=y,y=Ue()){case 34:case 39:case 91:case 40:x+=Ke(y);break;case 9:case 10:case 13:case 32:x+=Ye(b);break;case 92:x+=Xe(qe()-1,7);continue;case 47:switch(He()){case 42:case 47:De(et(Qe(Ue(),qe()),n,r),c);break;default:x+="/"}break;case 123*h:a[u++]=Pe(x)*g;case 125*h:case 59:case 0:switch(y){case 0:case 125:v=0;case 59+f:m>0&&Pe(x)-d&&De(m>32?tt(x+";",o,r,d-1):tt(Ee(x," ","")+";",o,r,d-2),c);break;case 59:x+=";";default:if(De(w=Ze(x,n,r,u,f,i,a,O,_=[],j=[],d),l),123===y)if(0===f)e(x,n,w,w,_,l,d,a,j);else switch(p){case 100:case 109:case 115:e(t,w,w,o&&De(Ze(t,w,w,0,0,i,a,O,i,_=[],d),j),i,j,d,a,o?_:j);break;default:e(x,w,w,w,[""],j,d,a,j)}}u=f=m=0,h=g=1,O=x="",d=s;break;case 58:d=1+Pe(x),m=b;default:if(h<1)if(123==y)--h;else if(125==y&&0==h++&&125==Ve())continue;switch(x+=we(y),y*h){case 38:g=f>0?1:(x+="\f",-1);break;case 44:a[u++]=(Pe(x)-1)*g,g=1;break;case 64:45===He()&&(x+=Ke(Ue())),p=He(),f=Pe(O=x+=Je(qe())),y++;break;case 45:45===b&&2==Pe(x)&&(h=0)}}return l}("",null,null,null,[""],e=We(e),0,[0],e))}(e?e+"{"+t.styles+"}":t.styles),f),r&&(d.inserted[t.name]=!0)};var d={key:t,sheet:new he({key:t,container:r,nonce:e.nonce,speedy:e.speedy,prepend:e.prepend}),nonce:e.nonce,inserted:l,registered:{},insert:o};return d.sheet.hydrate(s),d};function ft(e,t,n){var r="";return n.split(" ").forEach((function(n){void 0!==e[n]?t.push(e[n]+";"):r+=n+" "})),r}n(30);var dt=function(e,t,n){var r=e.key+"-"+t.name;if(!1===n&&void 0===e.registered[r]&&(e.registered[r]=t.styles),void 0===e.inserted[t.name]){var o=t;do{e.insert(t===o?"."+r:"",o,e.sheet,!0),o=o.next}while(void 0!==o)}},pt=function(e){for(var t,n=0,r=0,o=e.length;o>=4;++r,o-=4)t=1540483477*(65535&(t=255&e.charCodeAt(r)|(255&e.charCodeAt(++r))<<8|(255&e.charCodeAt(++r))<<16|(255&e.charCodeAt(++r))<<24))+(59797*(t>>>16)<<16),n=1540483477*(65535&(t^=t>>>24))+(59797*(t>>>16)<<16)^1540483477*(65535&n)+(59797*(n>>>16)<<16);switch(o){case 3:n^=(255&e.charCodeAt(r+2))<<16;case 2:n^=(255&e.charCodeAt(r+1))<<8;case 1:n=1540483477*(65535&(n^=255&e.charCodeAt(r)))+(59797*(n>>>16)<<16)}return(((n=1540483477*(65535&(n^=n>>>13))+(59797*(n>>>16)<<16))^n>>>15)>>>0).toString(36)},mt={animationIterationCount:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},bt=/[A-Z]|^ms/g,ht=/_EMO_([^_]+?)_([^]*?)_EMO_/g,vt=function(e){return 45===e.charCodeAt(1)},gt=function(e){return null!=e&&"boolean"!=typeof e},yt=it((function(e){return vt(e)?e:e.replace(bt,"-$&").toLowerCase()})),Ot=function(e,t){switch(e){case"animation":case"animationName":if("string"==typeof t)return t.replace(ht,(function(e,t,n){return jt={name:t,styles:n,next:jt},t}))}return 1===mt[e]||vt(e)||"number"!=typeof t||0===t?t:t+"px"};function _t(e,t,n){if(null==n)return"";if(void 0!==n.__emotion_styles)return n;switch(typeof n){case"boolean":return"";case"object":if(1===n.anim)return jt={name:n.name,styles:n.styles,next:jt},n.name;if(void 0!==n.styles){var r=n.next;if(void 0!==r)for(;void 0!==r;)jt={name:r.name,styles:r.styles,next:jt},r=r.next;return n.styles+";"}return function(e,t,n){var r="";if(Array.isArray(n))for(var o=0;o<n.length;o++)r+=_t(e,t,n[o])+";";else for(var i in n){var l=n[i];if("object"!=typeof l)null!=t&&void 0!==t[l]?r+=i+"{"+t[l]+"}":gt(l)&&(r+=yt(i)+":"+Ot(i,l)+";");else if(!Array.isArray(l)||"string"!=typeof l[0]||null!=t&&void 0!==t[l[0]]){var s=_t(e,t,l);switch(i){case"animation":case"animationName":r+=yt(i)+":"+s+";";break;default:r+=i+"{"+s+"}"}}else for(var a=0;a<l.length;a++)gt(l[a])&&(r+=yt(i)+":"+Ot(i,l[a])+";")}return r}(e,t,n);case"function":if(void 0!==e){var o=jt,i=n(e);return jt=o,_t(e,t,i)}}if(null==t)return n;var l=t[n];return void 0!==l?l:n}var jt,wt=/label:\s*([^\s;\n{]+)\s*(;|$)/g,xt=function(e,t,n){if(1===e.length&&"object"==typeof e[0]&&null!==e[0]&&void 0!==e[0].styles)return e[0];var r=!0,o="";jt=void 0;var i=e[0];null==i||void 0===i.raw?(r=!1,o+=_t(n,t,i)):o+=i[0];for(var l=1;l<e.length;l++)o+=_t(n,t,e[l]),r&&(o+=i[l]);wt.lastIndex=0;for(var s,a="";null!==(s=wt.exec(o));)a+="-"+s[1];return{name:pt(o)+a,styles:o,next:jt}},Et=Object.prototype.hasOwnProperty,kt=Object(me.createContext)("undefined"!=typeof HTMLElement?ut({key:"css"}):null),Ct=(kt.Provider,function(e){return Object(me.forwardRef)((function(t,n){var r=Object(me.useContext)(kt);return e(t,r,n)}))}),St=Object(me.createContext)({}),Pt="__EMOTION_TYPE_PLEASE_DO_NOT_USE__",It=function(e,t){var n={};for(var r in t)Et.call(t,r)&&(n[r]=t[r]);return n[Pt]=e,n},Dt=Ct((function(e,t,n){var r=e.css;"string"==typeof r&&void 0!==t.registered[r]&&(r=t.registered[r]);var o=e[Pt],i=[r],l="";"string"==typeof e.className?l=ft(t.registered,i,e.className):null!=e.className&&(l=e.className+" ");var s=xt(i,void 0,"function"==typeof r||Array.isArray(r)?Object(me.useContext)(St):void 0);dt(t,s,"string"==typeof o),l+=t.key+"-"+s.name;var a={};for(var c in e)Et.call(e,c)&&"css"!==c&&c!==Pt&&(a[c]=e[c]);return a.ref=n,a.className=l,Object(me.createElement)(o,a)})),Rt=n(25),Tt=n.n(Rt),Mt=function(e,t){var n=arguments;if(null==t||!Et.call(t,"css"))return me.createElement.apply(void 0,n);var r=n.length,o=new Array(r);o[0]=Dt,o[1]=It(e,t);for(var i=2;i<r;i++)o[i]=n[i];return me.createElement.apply(null,o)};function Lt(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return xt(t)}var At=function e(t){for(var n=t.length,r=0,o="";r<n;r++){var i=t[r];if(null!=i){var l=void 0;switch(typeof i){case"boolean":break;case"object":if(Array.isArray(i))l=e(i);else for(var s in l="",i)i[s]&&s&&(l&&(l+=" "),l+=s);break;default:l=i}l&&(o&&(o+=" "),o+=l)}}return o};function Nt(e,t,n){var r=[],o=ft(e,r,n);return r.length<2?n:o+t(r)}var Ft=Ct((function(e,t){var n=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];var o=xt(n,t.registered);return dt(t,o,!1),t.key+"-"+o.name},r={css:n,cx:function(){for(var e=arguments.length,r=new Array(e),o=0;o<e;o++)r[o]=arguments[o];return Nt(t.registered,n,At(r))},theme:Object(me.useContext)(St)};return e.children(r)}));function Bt(e){return(Bt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var Vt=n(29),Ut=n.n(Vt);function Ht(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function qt(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function zt(e,t,n){return t&&qt(e.prototype,t),n&&qt(e,n),e}function $t(e,t){return($t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function Wt(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&$t(e,t)}var Gt=n(24);function Kt(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function Yt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Xt(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Yt(Object(n),!0).forEach((function(t){Kt(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Yt(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function Qt(e){return(Qt=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function Jt(e,t){return!t||"object"!=typeof t&&"function"!=typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}function Zt(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=Qt(e);if(t){var o=Qt(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return Jt(this,n)}}var en=function(){};function tn(e,t){return t?"-"===t[0]?e+t:e+"__"+t:e}function nn(e,t,n){var r=[n];if(t&&e)for(var o in t)t.hasOwnProperty(o)&&t[o]&&r.push("".concat(tn(e,o)));return r.filter((function(e){return e})).map((function(e){return String(e).trim()})).join(" ")}var rn=function(e){return Array.isArray(e)?e.filter(Boolean):"object"===Bt(e)&&null!==e?[e]:[]},on=function(e){return e.className,e.clearValue,e.cx,e.getStyles,e.getValue,e.hasValue,e.isMulti,e.isRtl,e.options,e.selectOption,e.selectProps,e.setValue,e.theme,Xt({},de(e,["className","clearValue","cx","getStyles","getValue","hasValue","isMulti","isRtl","options","selectOption","selectProps","setValue","theme"]))};function ln(e){return[document.documentElement,document.body,window].indexOf(e)>-1}function sn(e){return ln(e)?window.pageYOffset:e.scrollTop}function an(e,t){ln(e)?window.scrollTo(0,t):e.scrollTop=t}function cn(e,t,n,r){return n*((e=e/r-1)*e*e+1)+t}function un(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:200,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:en,o=sn(e),i=t-o,l=10,s=0;function a(){var t=cn(s+=l,o,i,n);an(e,t),s<n?window.requestAnimationFrame(a):r(e)}a()}function fn(){try{return document.createEvent("TouchEvent"),!0}catch(e){return!1}}var dn=!1,pn={get passive(){return dn=!0}},mn="undefined"!=typeof window?window:{};mn.addEventListener&&mn.removeEventListener&&(mn.addEventListener("p",en,pn),mn.removeEventListener("p",en,!1));var bn=dn;function hn(e){var t=e.maxHeight,n=e.menuEl,r=e.minHeight,o=e.placement,i=e.shouldScroll,l=e.isFixedPosition,s=e.theme.spacing,a=function(e){var t=getComputedStyle(e),n="absolute"===t.position,r=/(auto|scroll)/,o=document.documentElement;if("fixed"===t.position)return o;for(var i=e;i=i.parentElement;)if(t=getComputedStyle(i),(!n||"static"!==t.position)&&r.test(t.overflow+t.overflowY+t.overflowX))return i;return o}(n),c={placement:"bottom",maxHeight:t};if(!n||!n.offsetParent)return c;var u=a.getBoundingClientRect().height,f=n.getBoundingClientRect(),d=f.bottom,p=f.height,m=f.top,b=n.offsetParent.getBoundingClientRect().top,h=window.innerHeight,v=sn(a),g=parseInt(getComputedStyle(n).marginBottom,10),y=parseInt(getComputedStyle(n).marginTop,10),O=b-y,_=h-m,j=O+v,w=u-v-m,x=d-h+v+g,E=v+m-y;switch(o){case"auto":case"bottom":if(_>=p)return{placement:"bottom",maxHeight:t};if(w>=p&&!l)return i&&un(a,x,160),{placement:"bottom",maxHeight:t};if(!l&&w>=r||l&&_>=r)return i&&un(a,x,160),{placement:"bottom",maxHeight:l?_-g:w-g};if("auto"===o||l){var k=t,C=l?O:j;return C>=r&&(k=Math.min(C-g-s.controlHeight,t)),{placement:"top",maxHeight:k}}if("bottom"===o)return i&&an(a,x),{placement:"bottom",maxHeight:t};break;case"top":if(O>=p)return{placement:"top",maxHeight:t};if(j>=p&&!l)return i&&un(a,E,160),{placement:"top",maxHeight:t};if(!l&&j>=r||l&&O>=r){var S=t;return(!l&&j>=r||l&&O>=r)&&(S=l?O-y:j-y),i&&un(a,E,160),{placement:"top",maxHeight:S}}return{placement:"bottom",maxHeight:t};default:throw new Error('Invalid placement provided "'.concat(o,'".'))}return c}var vn=function(e){return"auto"===e?"bottom":e},gn=Object(me.createContext)({getPortalPlacement:null}),yn=function(e){Wt(n,e);var t=Zt(n);function n(){var e;Ht(this,n);for(var r=arguments.length,o=new Array(r),i=0;i<r;i++)o[i]=arguments[i];return(e=t.call.apply(t,[this].concat(o))).state={maxHeight:e.props.maxMenuHeight,placement:null},e.getPlacement=function(t){var n=e.props,r=n.minMenuHeight,o=n.maxMenuHeight,i=n.menuPlacement,l=n.menuPosition,s=n.menuShouldScrollIntoView,a=n.theme;if(t){var c="fixed"===l,u=hn({maxHeight:o,menuEl:t,minHeight:r,placement:i,shouldScroll:s&&!c,isFixedPosition:c,theme:a}),f=e.context.getPortalPlacement;f&&f(u),e.setState(u)}},e.getUpdatedProps=function(){var t=e.props.menuPlacement,n=e.state.placement||vn(t);return Xt(Xt({},e.props),{},{placement:n,maxHeight:e.state.maxHeight})},e}return zt(n,[{key:"render",value:function(){return(0,this.props.children)({ref:this.getPlacement,placerProps:this.getUpdatedProps()})}}]),n}(me.Component);yn.contextType=gn;var On=function(e){var t=e.theme,n=t.spacing.baseUnit;return{color:t.colors.neutral40,padding:"".concat(2*n,"px ").concat(3*n,"px"),textAlign:"center"}},_n=On,jn=On,wn=function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps;return Mt("div",fe({css:o("noOptionsMessage",e),className:r({"menu-notice":!0,"menu-notice--no-options":!0},n)},i),t)};wn.defaultProps={children:"No options"};var xn=function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps;return Mt("div",fe({css:o("loadingMessage",e),className:r({"menu-notice":!0,"menu-notice--loading":!0},n)},i),t)};xn.defaultProps={children:"Loading..."};var En,kn,Cn,Sn=function(e){Wt(n,e);var t=Zt(n);function n(){var e;Ht(this,n);for(var r=arguments.length,o=new Array(r),i=0;i<r;i++)o[i]=arguments[i];return(e=t.call.apply(t,[this].concat(o))).state={placement:null},e.getPortalPlacement=function(t){var n=t.placement;n!==vn(e.props.menuPlacement)&&e.setState({placement:n})},e}return zt(n,[{key:"render",value:function(){var e=this.props,t=e.appendTo,n=e.children,r=e.className,o=e.controlElement,i=e.cx,l=e.innerProps,s=e.menuPlacement,a=e.menuPosition,c=e.getStyles,u="fixed"===a;if(!t&&!u||!o)return null;var f=this.state.placement||vn(s),d=function(e){var t=e.getBoundingClientRect();return{bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width}}(o),p=u?0:window.pageYOffset,m=d[f]+p,b=Mt("div",fe({css:c("menuPortal",{offset:m,position:a,rect:d}),className:i({"menu-portal":!0},r)},l),n);return Mt(gn.Provider,{value:{getPortalPlacement:this.getPortalPlacement}},t?Object(Gt.createPortal)(b,t):b)}}]),n}(me.Component),Pn={name:"8mmkcg",styles:"display:inline-block;fill:currentColor;line-height:1;stroke:currentColor;stroke-width:0"},In=function(e){var t=e.size,n=de(e,["size"]);return Mt("svg",fe({height:t,width:t,viewBox:"0 0 20 20","aria-hidden":"true",focusable:"false",css:Pn},n))},Dn=function(e){return Mt(In,fe({size:20},e),Mt("path",{d:"M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"}))},Rn=function(e){return Mt(In,fe({size:20},e),Mt("path",{d:"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"}))},Tn=function(e){var t=e.isFocused,n=e.theme,r=n.spacing.baseUnit,o=n.colors;return{label:"indicatorContainer",color:t?o.neutral60:o.neutral20,display:"flex",padding:2*r,transition:"color 150ms",":hover":{color:t?o.neutral80:o.neutral40}}},Mn=Tn,Ln=Tn,An=function(){var e=Lt.apply(void 0,arguments),t="animation-"+e.name;return{name:t,styles:"@keyframes "+t+"{"+e.styles+"}",anim:1,toString:function(){return"_EMO_"+this.name+"_"+this.styles+"_EMO_"}}}(En||(kn=["\n 0%, 80%, 100% { opacity: 0; }\n 40% { opacity: 1; }\n"],Cn||(Cn=kn.slice(0)),En=Object.freeze(Object.defineProperties(kn,{raw:{value:Object.freeze(Cn)}})))),Nn=function(e){var t=e.delay,n=e.offset;return Mt("span",{css:Lt({animation:"".concat(An," 1s ease-in-out ").concat(t,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:n?"1em":null,height:"1em",verticalAlign:"top",width:"1em"},"","")})},Fn=function(e){var t=e.className,n=e.cx,r=e.getStyles,o=e.innerProps,i=e.isRtl;return Mt("div",fe({css:r("loadingIndicator",e),className:n({indicator:!0,"loading-indicator":!0},t)},o),Mt(Nn,{delay:0,offset:i}),Mt(Nn,{delay:160,offset:!0}),Mt(Nn,{delay:320,offset:!i}))};Fn.defaultProps={size:4};var Bn=function(e){return{label:"input",background:0,border:0,fontSize:"inherit",opacity:e?0:1,outline:0,padding:0,color:"inherit"}},Vn=function(e){var t=e.children,n=e.innerProps;return Mt("div",n,t)},Un=Vn,Hn=Vn,qn=function(e){var t=e.children,n=e.className,r=e.components,o=e.cx,i=e.data,l=e.getStyles,s=e.innerProps,a=e.isDisabled,c=e.removeProps,u=e.selectProps,f=r.Container,d=r.Label,p=r.Remove;return Mt(Ft,null,(function(r){var m=r.css,b=r.cx;return Mt(f,{data:i,innerProps:Xt({className:b(m(l("multiValue",e)),o({"multi-value":!0,"multi-value--is-disabled":a},n))},s),selectProps:u},Mt(d,{data:i,innerProps:{className:b(m(l("multiValueLabel",e)),o({"multi-value__label":!0},n))},selectProps:u},t),Mt(p,{data:i,innerProps:Xt({className:b(m(l("multiValueRemove",e)),o({"multi-value__remove":!0},n))},c),selectProps:u}))}))};qn.defaultProps={cropWithEllipsis:!0};var zn={ClearIndicator:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps;return Mt("div",fe({css:o("clearIndicator",e),className:r({indicator:!0,"clear-indicator":!0},n)},i),t||Mt(Dn,null))},Control:function(e){var t=e.children,n=e.cx,r=e.getStyles,o=e.className,i=e.isDisabled,l=e.isFocused,s=e.innerRef,a=e.innerProps,c=e.menuIsOpen;return Mt("div",fe({ref:s,css:r("control",e),className:n({control:!0,"control--is-disabled":i,"control--is-focused":l,"control--menu-is-open":c},o)},a),t)},DropdownIndicator:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps;return Mt("div",fe({css:o("dropdownIndicator",e),className:r({indicator:!0,"dropdown-indicator":!0},n)},i),t||Mt(Rn,null))},DownChevron:Rn,CrossIcon:Dn,Group:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.Heading,l=e.headingProps,s=e.innerProps,a=e.label,c=e.theme,u=e.selectProps;return Mt("div",fe({css:o("group",e),className:r({group:!0},n)},s),Mt(i,fe({},l,{selectProps:u,theme:c,getStyles:o,cx:r}),a),Mt("div",null,t))},GroupHeading:function(e){var t=e.getStyles,n=e.cx,r=e.className,o=on(e);o.data;var i=de(o,["data"]);return Mt("div",fe({css:t("groupHeading",e),className:n({"group-heading":!0},r)},i))},IndicatorsContainer:function(e){var t=e.children,n=e.className,r=e.cx,o=e.innerProps,i=e.getStyles;return Mt("div",fe({css:i("indicatorsContainer",e),className:r({indicators:!0},n)},o),t)},IndicatorSeparator:function(e){var t=e.className,n=e.cx,r=e.getStyles,o=e.innerProps;return Mt("span",fe({},o,{css:r("indicatorSeparator",e),className:n({"indicator-separator":!0},t)}))},Input:function(e){var t=e.className,n=e.cx,r=e.getStyles,o=on(e),i=o.innerRef,l=o.isDisabled,s=o.isHidden,a=de(o,["innerRef","isDisabled","isHidden"]);return Mt("div",{css:r("input",e)},Mt(Ut.a,fe({className:n({input:!0},t),inputRef:i,inputStyle:Bn(s),disabled:l},a)))},LoadingIndicator:Fn,Menu:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerRef,l=e.innerProps;return Mt("div",fe({css:o("menu",e),className:r({menu:!0},n),ref:i},l),t)},MenuList:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps,l=e.innerRef,s=e.isMulti;return Mt("div",fe({css:o("menuList",e),className:r({"menu-list":!0,"menu-list--is-multi":s},n),ref:l},i),t)},MenuPortal:Sn,LoadingMessage:xn,NoOptionsMessage:wn,MultiValue:qn,MultiValueContainer:Un,MultiValueLabel:Hn,MultiValueRemove:function(e){var t=e.children,n=e.innerProps;return Mt("div",n,t||Mt(Dn,{size:14}))},Option:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.isDisabled,l=e.isFocused,s=e.isSelected,a=e.innerRef,c=e.innerProps;return Mt("div",fe({css:o("option",e),className:r({option:!0,"option--is-disabled":i,"option--is-focused":l,"option--is-selected":s},n),ref:a},c),t)},Placeholder:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps;return Mt("div",fe({css:o("placeholder",e),className:r({placeholder:!0},n)},i),t)},SelectContainer:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.innerProps,l=e.isDisabled,s=e.isRtl;return Mt("div",fe({css:o("container",e),className:r({"--is-disabled":l,"--is-rtl":s},n)},i),t)},SingleValue:function(e){var t=e.children,n=e.className,r=e.cx,o=e.getStyles,i=e.isDisabled,l=e.innerProps;return Mt("div",fe({css:o("singleValue",e),className:r({"single-value":!0,"single-value--is-disabled":i},n)},l),t)},ValueContainer:function(e){var t=e.children,n=e.className,r=e.cx,o=e.innerProps,i=e.isMulti,l=e.getStyles,s=e.hasValue;return Mt("div",fe({css:l("valueContainer",e),className:r({"value-container":!0,"value-container--is-multi":i,"value-container--has-value":s},n)},o),t)}};function $n(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function Wn(e){return function(e){if(Array.isArray(e))return $n(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return $n(e,void 0);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?$n(e,void 0):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}var Gn=Number.isNaN||function(e){return"number"==typeof e&&e!=e};function Kn(e,t){if(e.length!==t.length)return!1;for(var n=0;n<e.length;n++)if(!((r=e[n])===(o=t[n])||Gn(r)&&Gn(o)))return!1;var r,o;return!0}for(var Yn={name:"7pg0cj-a11yText",styles:"label:a11yText;z-index:9999;border:0;clip:rect(1px, 1px, 1px, 1px);height:1px;width:1px;position:absolute;overflow:hidden;padding:0;white-space:nowrap"},Xn=function(e){return Mt("span",fe({css:Yn},e))},Qn={guidance:function(e){var t=e.isSearchable,n=e.isMulti,r=e.isDisabled,o=e.tabSelectsValue;switch(e.context){case"menu":return"Use Up and Down to choose options".concat(r?"":", press Enter to select the currently focused option",", press Escape to exit the menu").concat(o?", press Tab to select the option and exit the menu":"",".");case"input":return"".concat(e["aria-label"]||"Select"," is focused ").concat(t?",type to refine list":"",", press Down to open the menu, ").concat(n?" press left to focus selected values":"");case"value":return"Use left and right to toggle between focused values, press Backspace to remove the currently focused value";default:return""}},onChange:function(e){var t=e.action,n=e.label,r=void 0===n?"":n,o=e.isDisabled;switch(t){case"deselect-option":case"pop-value":case"remove-value":return"option ".concat(r,", deselected.");case"select-option":return"option ".concat(r,o?" is disabled. Select another option.":", selected.");default:return""}},onFocus:function(e){var t=e.context,n=e.focused,r=void 0===n?{}:n,o=e.options,i=e.label,l=void 0===i?"":i,s=e.selectValue,a=e.isDisabled,c=e.isSelected,u=function(e,t){return e&&e.length?"".concat(e.indexOf(t)+1," of ").concat(e.length):""};if("value"===t&&s)return"value ".concat(l," focused, ").concat(u(s,r),".");if("menu"===t){var f=a?" disabled":"",d="".concat(c?"selected":"focused").concat(f);return"option ".concat(l," ").concat(d,", ").concat(u(o,r),".")}return""},onFilter:function(e){var t=e.inputValue,n=e.resultsMessage;return"".concat(n).concat(t?" for search term "+t:"",".")}},Jn=function(e){var t=e.ariaSelection,n=e.focusedOption,r=e.focusedValue,o=e.focusableOptions,i=e.isFocused,l=e.selectValue,s=e.selectProps,a=s.ariaLiveMessages,c=s.getOptionLabel,u=s.inputValue,f=s.isMulti,d=s.isOptionDisabled,p=s.isSearchable,m=s.menuIsOpen,b=s.options,h=s.screenReaderStatus,v=s.tabSelectsValue,g=s["aria-label"],y=s["aria-live"],O=Object(me.useMemo)((function(){return Xt(Xt({},Qn),a||{})}),[a]),_=Object(me.useMemo)((function(){var e,n="";if(t&&O.onChange){var r=t.option,o=t.removedValue,i=t.value,l=o||r||(e=i,Array.isArray(e)?null:e),s=Xt({isDisabled:l&&d(l),label:l?c(l):""},t);n=O.onChange(s)}return n}),[t,d,c,O]),j=Object(me.useMemo)((function(){var e="",t=n||r,o=!!(n&&l&&l.includes(n));if(t&&O.onFocus){var i={focused:t,label:c(t),isDisabled:d(t),isSelected:o,options:b,context:t===n?"menu":"value",selectValue:l};e=O.onFocus(i)}return e}),[n,r,c,d,O,b,l]),w=Object(me.useMemo)((function(){var e="";if(m&&b.length&&O.onFilter){var t=h({count:o.length});e=O.onFilter({inputValue:u,resultsMessage:t})}return e}),[o,u,m,O,b,h]),x=Object(me.useMemo)((function(){var e="";if(O.guidance){var t=r?"value":m?"menu":"input";e=O.guidance({"aria-label":g,context:t,isDisabled:n&&d(n),isMulti:f,isSearchable:p,tabSelectsValue:v})}return e}),[g,n,r,f,d,p,m,O,v]),E="".concat(j," ").concat(w," ").concat(x);return Mt(Xn,{"aria-live":y,"aria-atomic":"false","aria-relevant":"additions text"},i&&Mt(be.a.Fragment,null,Mt("span",{id:"aria-selection"},_),Mt("span",{id:"aria-context"},E)))},Zn=[{base:"A",letters:"AⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ"},{base:"AA",letters:"Ꜳ"},{base:"AE",letters:"ÆǼǢ"},{base:"AO",letters:"Ꜵ"},{base:"AU",letters:"Ꜷ"},{base:"AV",letters:"ꜸꜺ"},{base:"AY",letters:"Ꜽ"},{base:"B",letters:"BⒷBḂḄḆɃƂƁ"},{base:"C",letters:"CⒸCĆĈĊČÇḈƇȻꜾ"},{base:"D",letters:"DⒹDḊĎḌḐḒḎĐƋƊƉꝹ"},{base:"DZ",letters:"DZDŽ"},{base:"Dz",letters:"DzDž"},{base:"E",letters:"EⒺEÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ"},{base:"F",letters:"FⒻFḞƑꝻ"},{base:"G",letters:"GⒼGǴĜḠĞĠǦĢǤƓꞠꝽꝾ"},{base:"H",letters:"HⒽHĤḢḦȞḤḨḪĦⱧⱵꞍ"},{base:"I",letters:"IⒾIÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ"},{base:"J",letters:"JⒿJĴɈ"},{base:"K",letters:"KⓀKḰǨḲĶḴƘⱩꝀꝂꝄꞢ"},{base:"L",letters:"LⓁLĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ"},{base:"LJ",letters:"LJ"},{base:"Lj",letters:"Lj"},{base:"M",letters:"MⓂMḾṀṂⱮƜ"},{base:"N",letters:"NⓃNǸŃÑṄŇṆŅṊṈȠƝꞐꞤ"},{base:"NJ",letters:"NJ"},{base:"Nj",letters:"Nj"},{base:"O",letters:"OⓄOÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ"},{base:"OI",letters:"Ƣ"},{base:"OO",letters:"Ꝏ"},{base:"OU",letters:"Ȣ"},{base:"P",letters:"PⓅPṔṖƤⱣꝐꝒꝔ"},{base:"Q",letters:"QⓆQꝖꝘɊ"},{base:"R",letters:"RⓇRŔṘŘȐȒṚṜŖṞɌⱤꝚꞦꞂ"},{base:"S",letters:"SⓈSẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ"},{base:"T",letters:"TⓉTṪŤṬȚŢṰṮŦƬƮȾꞆ"},{base:"TZ",letters:"Ꜩ"},{base:"U",letters:"UⓊUÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ"},{base:"V",letters:"VⓋVṼṾƲꝞɅ"},{base:"VY",letters:"Ꝡ"},{base:"W",letters:"WⓌWẀẂŴẆẄẈⱲ"},{base:"X",letters:"XⓍXẊẌ"},{base:"Y",letters:"YⓎYỲÝŶỸȲẎŸỶỴƳɎỾ"},{base:"Z",letters:"ZⓏZŹẐŻŽẒẔƵȤⱿⱫꝢ"},{base:"a",letters:"aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ"},{base:"aa",letters:"ꜳ"},{base:"ae",letters:"æǽǣ"},{base:"ao",letters:"ꜵ"},{base:"au",letters:"ꜷ"},{base:"av",letters:"ꜹꜻ"},{base:"ay",letters:"ꜽ"},{base:"b",letters:"bⓑbḃḅḇƀƃɓ"},{base:"c",letters:"cⓒcćĉċčçḉƈȼꜿↄ"},{base:"d",letters:"dⓓdḋďḍḑḓḏđƌɖɗꝺ"},{base:"dz",letters:"dzdž"},{base:"e",letters:"eⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ"},{base:"f",letters:"fⓕfḟƒꝼ"},{base:"g",letters:"gⓖgǵĝḡğġǧģǥɠꞡᵹꝿ"},{base:"h",letters:"hⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ"},{base:"hv",letters:"ƕ"},{base:"i",letters:"iⓘiìíîĩīĭïḯỉǐȉȋịįḭɨı"},{base:"j",letters:"jⓙjĵǰɉ"},{base:"k",letters:"kⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ"},{base:"l",letters:"lⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ"},{base:"lj",letters:"lj"},{base:"m",letters:"mⓜmḿṁṃɱɯ"},{base:"n",letters:"nⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ"},{base:"nj",letters:"nj"},{base:"o",letters:"oⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ"},{base:"oi",letters:"ƣ"},{base:"ou",letters:"ȣ"},{base:"oo",letters:"ꝏ"},{base:"p",letters:"pⓟpṕṗƥᵽꝑꝓꝕ"},{base:"q",letters:"qⓠqɋꝗꝙ"},{base:"r",letters:"rⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ"},{base:"s",letters:"sⓢsßśṥŝṡšṧṣṩșşȿꞩꞅẛ"},{base:"t",letters:"tⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ"},{base:"tz",letters:"ꜩ"},{base:"u",letters:"uⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ"},{base:"v",letters:"vⓥvṽṿʋꝟʌ"},{base:"vy",letters:"ꝡ"},{base:"w",letters:"wⓦwẁẃŵẇẅẘẉⱳ"},{base:"x",letters:"xⓧxẋẍ"},{base:"y",letters:"yⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ"},{base:"z",letters:"zⓩzźẑżžẓẕƶȥɀⱬꝣ"}],er=new RegExp("["+Zn.map((function(e){return e.letters})).join("")+"]","g"),tr={},nr=0;nr<Zn.length;nr++)for(var rr=Zn[nr],or=0;or<rr.letters.length;or++)tr[rr.letters[or]]=rr.base;var ir=function(e){return e.replace(er,(function(e){return tr[e]}))},lr=function(e,t){var n;void 0===t&&(t=Kn);var r,o=[],i=!1;return function(){for(var l=[],s=0;s<arguments.length;s++)l[s]=arguments[s];return i&&n===this&&t(l,o)||(r=e.apply(this,l),i=!0,n=this,o=l),r}}(ir),sr=function(e){return e.replace(/^\s+|\s+$/g,"")},ar=function(e){return"".concat(e.label," ").concat(e.value)};function cr(e){e.in,e.out,e.onExited,e.appear,e.enter,e.exit;var t=e.innerRef;e.emotion;var n=de(e,["in","out","onExited","appear","enter","exit","innerRef","emotion"]);return Mt("input",fe({ref:t},n,{css:Lt({label:"dummyInput",background:0,border:0,fontSize:"inherit",outline:0,padding:0,width:1,color:"transparent",left:-100,opacity:0,position:"relative",transform:"scale(0)"},"","")}))}var ur=["boxSizing","height","overflow","paddingRight","position"],fr={boxSizing:"border-box",overflow:"hidden",position:"relative",height:"100%"};function dr(e){e.preventDefault()}function pr(e){e.stopPropagation()}function mr(){var e=this.scrollTop,t=this.scrollHeight,n=e+this.offsetHeight;0===e?this.scrollTop=1:n===t&&(this.scrollTop=e-1)}function br(){return"ontouchstart"in window||navigator.maxTouchPoints}var hr=!("undefined"==typeof window||!window.document||!window.document.createElement),vr=0,gr={capture:!1,passive:!1},yr=function(){return document.activeElement&&document.activeElement.blur()},Or={name:"1kfdb0e",styles:"position:fixed;left:0;bottom:0;right:0;top:0"};function _r(e){var t=e.children,n=e.lockEnabled,r=e.captureEnabled,o=function(e){var t=e.isEnabled,n=e.onBottomArrive,r=e.onBottomLeave,o=e.onTopArrive,i=e.onTopLeave,l=Object(me.useRef)(!1),s=Object(me.useRef)(!1),a=Object(me.useRef)(0),c=Object(me.useRef)(null),u=Object(me.useCallback)((function(e,t){if(null!==c.current){var a=c.current,u=a.scrollTop,f=a.scrollHeight,d=a.clientHeight,p=c.current,m=t>0,b=f-d-u,h=!1;b>t&&l.current&&(r&&r(e),l.current=!1),m&&s.current&&(i&&i(e),s.current=!1),m&&t>b?(n&&!l.current&&n(e),p.scrollTop=f,h=!0,l.current=!0):!m&&-t>u&&(o&&!s.current&&o(e),p.scrollTop=0,h=!0,s.current=!0),h&&function(e){e.preventDefault(),e.stopPropagation()}(e)}}),[]),f=Object(me.useCallback)((function(e){u(e,e.deltaY)}),[u]),d=Object(me.useCallback)((function(e){a.current=e.changedTouches[0].clientY}),[]),p=Object(me.useCallback)((function(e){var t=a.current-e.changedTouches[0].clientY;u(e,t)}),[u]),m=Object(me.useCallback)((function(e){if(e){var t=!!bn&&{passive:!1};"function"==typeof e.addEventListener&&e.addEventListener("wheel",f,t),"function"==typeof e.addEventListener&&e.addEventListener("touchstart",d,t),"function"==typeof e.addEventListener&&e.addEventListener("touchmove",p,t)}}),[p,d,f]),b=Object(me.useCallback)((function(e){e&&("function"==typeof e.removeEventListener&&e.removeEventListener("wheel",f,!1),"function"==typeof e.removeEventListener&&e.removeEventListener("touchstart",d,!1),"function"==typeof e.removeEventListener&&e.removeEventListener("touchmove",p,!1))}),[p,d,f]);return Object(me.useEffect)((function(){if(t){var e=c.current;return m(e),function(){b(e)}}}),[t,m,b]),function(e){c.current=e}}({isEnabled:void 0===r||r,onBottomArrive:e.onBottomArrive,onBottomLeave:e.onBottomLeave,onTopArrive:e.onTopArrive,onTopLeave:e.onTopLeave}),i=function(e){var t=e.isEnabled,n=e.accountForScrollbars,r=void 0===n||n,o=Object(me.useRef)({}),i=Object(me.useRef)(null),l=Object(me.useCallback)((function(e){if(hr){var t=document.body,n=t&&t.style;if(r&&ur.forEach((function(e){var t=n&&n[e];o.current[e]=t})),r&&vr<1){var i=parseInt(o.current.paddingRight,10)||0,l=document.body?document.body.clientWidth:0,s=window.innerWidth-l+i||0;Object.keys(fr).forEach((function(e){var t=fr[e];n&&(n[e]=t)})),n&&(n.paddingRight="".concat(s,"px"))}t&&br()&&(t.addEventListener("touchmove",dr,gr),e&&(e.addEventListener("touchstart",mr,gr),e.addEventListener("touchmove",pr,gr))),vr+=1}}),[]),s=Object(me.useCallback)((function(e){if(hr){var t=document.body,n=t&&t.style;vr=Math.max(vr-1,0),r&&vr<1&&ur.forEach((function(e){var t=o.current[e];n&&(n[e]=t)})),t&&br()&&(t.removeEventListener("touchmove",dr,gr),e&&(e.removeEventListener("touchstart",mr,gr),e.removeEventListener("touchmove",pr,gr)))}}),[]);return Object(me.useEffect)((function(){if(t){var e=i.current;return l(e),function(){s(e)}}}),[t,l,s]),function(e){i.current=e}}({isEnabled:n});return Mt(be.a.Fragment,null,n&&Mt("div",{onClick:yr,css:Or}),t((function(e){o(e),i(e)})))}var jr={clearIndicator:Ln,container:function(e){var t=e.isDisabled;return{label:"container",direction:e.isRtl?"rtl":null,pointerEvents:t?"none":null,position:"relative"}},control:function(e){var t=e.isDisabled,n=e.isFocused,r=e.theme,o=r.colors,i=r.borderRadius,l=r.spacing;return{label:"control",alignItems:"center",backgroundColor:t?o.neutral5:o.neutral0,borderColor:t?o.neutral10:n?o.primary:o.neutral20,borderRadius:i,borderStyle:"solid",borderWidth:1,boxShadow:n?"0 0 0 1px ".concat(o.primary):null,cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms","&:hover":{borderColor:n?o.primary:o.neutral30}}},dropdownIndicator:Mn,group:function(e){var t=e.theme.spacing;return{paddingBottom:2*t.baseUnit,paddingTop:2*t.baseUnit}},groupHeading:function(e){var t=e.theme.spacing;return{label:"group",color:"#999",cursor:"default",display:"block",fontSize:"75%",fontWeight:"500",marginBottom:"0.25em",paddingLeft:3*t.baseUnit,paddingRight:3*t.baseUnit,textTransform:"uppercase"}},indicatorsContainer:function(){return{alignItems:"center",alignSelf:"stretch",display:"flex",flexShrink:0}},indicatorSeparator:function(e){var t=e.isDisabled,n=e.theme,r=n.spacing.baseUnit,o=n.colors;return{label:"indicatorSeparator",alignSelf:"stretch",backgroundColor:t?o.neutral10:o.neutral20,marginBottom:2*r,marginTop:2*r,width:1}},input:function(e){var t=e.isDisabled,n=e.theme,r=n.spacing,o=n.colors;return{margin:r.baseUnit/2,paddingBottom:r.baseUnit/2,paddingTop:r.baseUnit/2,visibility:t?"hidden":"visible",color:o.neutral80}},loadingIndicator:function(e){var t=e.isFocused,n=e.size,r=e.theme,o=r.colors,i=r.spacing.baseUnit;return{label:"loadingIndicator",color:t?o.neutral60:o.neutral20,display:"flex",padding:2*i,transition:"color 150ms",alignSelf:"center",fontSize:n,lineHeight:1,marginRight:n,textAlign:"center",verticalAlign:"middle"}},loadingMessage:jn,menu:function(e){var t,n=e.placement,r=e.theme,o=r.borderRadius,i=r.spacing,l=r.colors;return t={label:"menu"},Object(pe.a)(t,function(e){return e?{bottom:"top",top:"bottom"}[e]:"bottom"}(n),"100%"),Object(pe.a)(t,"backgroundColor",l.neutral0),Object(pe.a)(t,"borderRadius",o),Object(pe.a)(t,"boxShadow","0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1)"),Object(pe.a)(t,"marginBottom",i.menuGutter),Object(pe.a)(t,"marginTop",i.menuGutter),Object(pe.a)(t,"position","absolute"),Object(pe.a)(t,"width","100%"),Object(pe.a)(t,"zIndex",1),t},menuList:function(e){var t=e.maxHeight,n=e.theme.spacing.baseUnit;return{maxHeight:t,overflowY:"auto",paddingBottom:n,paddingTop:n,position:"relative",WebkitOverflowScrolling:"touch"}},menuPortal:function(e){var t=e.rect,n=e.offset,r=e.position;return{left:t.left,position:r,top:n,width:t.width,zIndex:1}},multiValue:function(e){var t=e.theme,n=t.spacing,r=t.borderRadius;return{label:"multiValue",backgroundColor:t.colors.neutral10,borderRadius:r/2,display:"flex",margin:n.baseUnit/2,minWidth:0}},multiValueLabel:function(e){var t=e.theme,n=t.borderRadius,r=t.colors,o=e.cropWithEllipsis;return{borderRadius:n/2,color:r.neutral80,fontSize:"85%",overflow:"hidden",padding:3,paddingLeft:6,textOverflow:o?"ellipsis":null,whiteSpace:"nowrap"}},multiValueRemove:function(e){var t=e.theme,n=t.spacing,r=t.borderRadius,o=t.colors;return{alignItems:"center",borderRadius:r/2,backgroundColor:e.isFocused&&o.dangerLight,display:"flex",paddingLeft:n.baseUnit,paddingRight:n.baseUnit,":hover":{backgroundColor:o.dangerLight,color:o.danger}}},noOptionsMessage:_n,option:function(e){var t=e.isDisabled,n=e.isFocused,r=e.isSelected,o=e.theme,i=o.spacing,l=o.colors;return{label:"option",backgroundColor:r?l.primary:n?l.primary25:"transparent",color:t?l.neutral20:r?l.neutral0:"inherit",cursor:"default",display:"block",fontSize:"inherit",padding:"".concat(2*i.baseUnit,"px ").concat(3*i.baseUnit,"px"),width:"100%",userSelect:"none",WebkitTapHighlightColor:"rgba(0, 0, 0, 0)",":active":{backgroundColor:!t&&(r?l.primary:l.primary50)}}},placeholder:function(e){var t=e.theme,n=t.spacing;return{label:"placeholder",color:t.colors.neutral50,marginLeft:n.baseUnit/2,marginRight:n.baseUnit/2,position:"absolute",top:"50%",transform:"translateY(-50%)"}},singleValue:function(e){var t=e.isDisabled,n=e.theme,r=n.spacing,o=n.colors;return{label:"singleValue",color:t?o.neutral40:o.neutral80,marginLeft:r.baseUnit/2,marginRight:r.baseUnit/2,maxWidth:"calc(100% - ".concat(2*r.baseUnit,"px)"),overflow:"hidden",position:"absolute",textOverflow:"ellipsis",whiteSpace:"nowrap",top:"50%",transform:"translateY(-50%)"}},valueContainer:function(e){var t=e.theme.spacing;return{alignItems:"center",display:"flex",flex:1,flexWrap:"wrap",padding:"".concat(t.baseUnit/2,"px ").concat(2*t.baseUnit,"px"),WebkitOverflowScrolling:"touch",position:"relative",overflow:"hidden"}}},wr={borderRadius:4,colors:{primary:"#2684FF",primary75:"#4C9AFF",primary50:"#B2D4FF",primary25:"#DEEBFF",danger:"#DE350B",dangerLight:"#FFBDAD",neutral0:"hsl(0, 0%, 100%)",neutral5:"hsl(0, 0%, 95%)",neutral10:"hsl(0, 0%, 90%)",neutral20:"hsl(0, 0%, 80%)",neutral30:"hsl(0, 0%, 70%)",neutral40:"hsl(0, 0%, 60%)",neutral50:"hsl(0, 0%, 50%)",neutral60:"hsl(0, 0%, 40%)",neutral70:"hsl(0, 0%, 30%)",neutral80:"hsl(0, 0%, 20%)",neutral90:"hsl(0, 0%, 10%)"},spacing:{baseUnit:4,controlHeight:38,menuGutter:8}},xr={"aria-live":"polite",backspaceRemovesValue:!0,blurInputOnSelect:fn(),captureMenuScroll:!fn(),closeMenuOnSelect:!0,closeMenuOnScroll:!1,components:{},controlShouldRenderValue:!0,escapeClearsValue:!1,filterOption:function(e,t){var n=Xt({ignoreCase:!0,ignoreAccents:!0,stringify:ar,trim:!0,matchFrom:"any"},void 0),r=n.ignoreCase,o=n.ignoreAccents,i=n.stringify,l=n.trim,s=n.matchFrom,a=l?sr(t):t,c=l?sr(i(e)):i(e);return r&&(a=a.toLowerCase(),c=c.toLowerCase()),o&&(a=lr(a),c=ir(c)),"start"===s?c.substr(0,a.length)===a:c.indexOf(a)>-1},formatGroupLabel:function(e){return e.label},getOptionLabel:function(e){return e.label},getOptionValue:function(e){return e.value},isDisabled:!1,isLoading:!1,isMulti:!1,isRtl:!1,isSearchable:!0,isOptionDisabled:function(e){return!!e.isDisabled},loadingMessage:function(){return"Loading..."},maxMenuHeight:300,minMenuHeight:140,menuIsOpen:!1,menuPlacement:"bottom",menuPosition:"absolute",menuShouldBlockScroll:!1,menuShouldScrollIntoView:!function(){try{return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}catch(e){return!1}}(),noOptionsMessage:function(){return"No options"},openMenuOnFocus:!1,openMenuOnClick:!0,options:[],pageSize:5,placeholder:"Select...",screenReaderStatus:function(e){var t=e.count;return"".concat(t," result").concat(1!==t?"s":""," available")},styles:{},tabIndex:"0",tabSelectsValue:!0};function Er(e,t,n,r){return{type:"option",data:t,isDisabled:Dr(e,t,n),isSelected:Rr(e,t,n),label:Pr(e,t),value:Ir(e,t),index:r}}function kr(e,t){return e.options.map((function(n,r){if(n.options){var o=n.options.map((function(n,r){return Er(e,n,t,r)})).filter((function(t){return Sr(e,t)}));return o.length>0?{type:"group",data:n,options:o,index:r}:void 0}var i=Er(e,n,t,r);return Sr(e,i)?i:void 0})).filter((function(e){return!!e}))}function Cr(e){return e.reduce((function(e,t){return"group"===t.type?e.push.apply(e,Wn(t.options.map((function(e){return e.data})))):e.push(t.data),e}),[])}function Sr(e,t){var n=e.inputValue,r=void 0===n?"":n,o=t.data,i=t.isSelected,l=t.label,s=t.value;return(!Mr(e)||!i)&&Tr(e,{label:l,value:s,data:o},r)}var Pr=function(e,t){return e.getOptionLabel(t)},Ir=function(e,t){return e.getOptionValue(t)};function Dr(e,t,n){return"function"==typeof e.isOptionDisabled&&e.isOptionDisabled(t,n)}function Rr(e,t,n){if(n.indexOf(t)>-1)return!0;if("function"==typeof e.isOptionSelected)return e.isOptionSelected(t,n);var r=Ir(e,t);return n.some((function(t){return Ir(e,t)===r}))}function Tr(e,t,n){return!e.filterOption||e.filterOption(t,n)}var Mr=function(e){var t=e.hideSelectedOptions,n=e.isMulti;return void 0===t?n:t},Lr=1,Ar=function(e){Wt(n,e);var t=Zt(n);function n(e){var r;return Ht(this,n),(r=t.call(this,e)).state={ariaSelection:null,focusedOption:null,focusedValue:null,inputIsHidden:!1,isFocused:!1,selectValue:[],clearFocusValueOnUpdate:!1,inputIsHiddenAfterUpdate:void 0,prevProps:void 0},r.blockOptionHover=!1,r.isComposing=!1,r.commonProps=void 0,r.initialTouchX=0,r.initialTouchY=0,r.instancePrefix="",r.openAfterFocus=!1,r.scrollToFocusedOptionOnUpdate=!1,r.userIsDragging=void 0,r.controlRef=null,r.getControlRef=function(e){r.controlRef=e},r.focusedOptionRef=null,r.getFocusedOptionRef=function(e){r.focusedOptionRef=e},r.menuListRef=null,r.getMenuListRef=function(e){r.menuListRef=e},r.inputRef=null,r.getInputRef=function(e){r.inputRef=e},r.focus=r.focusInput,r.blur=r.blurInput,r.onChange=function(e,t){var n=r.props,o=n.onChange,i=n.name;t.name=i,r.ariaOnChange(e,t),o(e,t)},r.setValue=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"set-value",n=arguments.length>2?arguments[2]:void 0,o=r.props,i=o.closeMenuOnSelect,l=o.isMulti;r.onInputChange("",{action:"set-value"}),i&&(r.setState({inputIsHiddenAfterUpdate:!l}),r.onMenuClose()),r.setState({clearFocusValueOnUpdate:!0}),r.onChange(e,{action:t,option:n})},r.selectOption=function(e){var t=r.props,n=t.blurInputOnSelect,o=t.isMulti,i=t.name,l=r.state.selectValue,s=o&&r.isOptionSelected(e,l),a=r.isOptionDisabled(e,l);if(s){var c=r.getOptionValue(e);r.setValue(l.filter((function(e){return r.getOptionValue(e)!==c})),"deselect-option",e)}else{if(a)return void r.ariaOnChange(e,{action:"select-option",name:i});o?r.setValue([].concat(Wn(l),[e]),"select-option",e):r.setValue(e,"select-option")}n&&r.blurInput()},r.removeValue=function(e){var t=r.props.isMulti,n=r.state.selectValue,o=r.getOptionValue(e),i=n.filter((function(e){return r.getOptionValue(e)!==o})),l=t?i:i[0]||null;r.onChange(l,{action:"remove-value",removedValue:e}),r.focusInput()},r.clearValue=function(){var e=r.state.selectValue;r.onChange(r.props.isMulti?[]:null,{action:"clear",removedValues:e})},r.popValue=function(){var e=r.props.isMulti,t=r.state.selectValue,n=t[t.length-1],o=t.slice(0,t.length-1),i=e?o:o[0]||null;r.onChange(i,{action:"pop-value",removedValue:n})},r.getValue=function(){return r.state.selectValue},r.cx=function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return nn.apply(void 0,[r.props.classNamePrefix].concat(t))},r.getOptionLabel=function(e){return Pr(r.props,e)},r.getOptionValue=function(e){return Ir(r.props,e)},r.getStyles=function(e,t){var n=jr[e](t);n.boxSizing="border-box";var o=r.props.styles[e];return o?o(n,t):n},r.getElementId=function(e){return"".concat(r.instancePrefix,"-").concat(e)},r.getComponents=function(){return e=r.props,Xt(Xt({},zn),e.components);var e},r.buildCategorizedOptions=function(){return kr(r.props,r.state.selectValue)},r.getCategorizedOptions=function(){return r.props.menuIsOpen?r.buildCategorizedOptions():[]},r.buildFocusableOptions=function(){return Cr(r.buildCategorizedOptions())},r.getFocusableOptions=function(){return r.props.menuIsOpen?r.buildFocusableOptions():[]},r.ariaOnChange=function(e,t){r.setState({ariaSelection:Xt({value:e},t)})},r.onMenuMouseDown=function(e){0===e.button&&(e.stopPropagation(),e.preventDefault(),r.focusInput())},r.onMenuMouseMove=function(e){r.blockOptionHover=!1},r.onControlMouseDown=function(e){var t=r.props.openMenuOnClick;r.state.isFocused?r.props.menuIsOpen?"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&r.onMenuClose():t&&r.openMenu("first"):(t&&(r.openAfterFocus=!0),r.focusInput()),"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&e.preventDefault()},r.onDropdownIndicatorMouseDown=function(e){if(!(e&&"mousedown"===e.type&&0!==e.button||r.props.isDisabled)){var t=r.props,n=t.isMulti,o=t.menuIsOpen;r.focusInput(),o?(r.setState({inputIsHiddenAfterUpdate:!n}),r.onMenuClose()):r.openMenu("first"),e.preventDefault(),e.stopPropagation()}},r.onClearIndicatorMouseDown=function(e){e&&"mousedown"===e.type&&0!==e.button||(r.clearValue(),e.stopPropagation(),r.openAfterFocus=!1,"touchend"===e.type?r.focusInput():setTimeout((function(){return r.focusInput()})))},r.onScroll=function(e){"boolean"==typeof r.props.closeMenuOnScroll?e.target instanceof HTMLElement&&ln(e.target)&&r.props.onMenuClose():"function"==typeof r.props.closeMenuOnScroll&&r.props.closeMenuOnScroll(e)&&r.props.onMenuClose()},r.onCompositionStart=function(){r.isComposing=!0},r.onCompositionEnd=function(){r.isComposing=!1},r.onTouchStart=function(e){var t=e.touches,n=t&&t.item(0);n&&(r.initialTouchX=n.clientX,r.initialTouchY=n.clientY,r.userIsDragging=!1)},r.onTouchMove=function(e){var t=e.touches,n=t&&t.item(0);if(n){var o=Math.abs(n.clientX-r.initialTouchX),i=Math.abs(n.clientY-r.initialTouchY);r.userIsDragging=o>5||i>5}},r.onTouchEnd=function(e){r.userIsDragging||(r.controlRef&&!r.controlRef.contains(e.target)&&r.menuListRef&&!r.menuListRef.contains(e.target)&&r.blurInput(),r.initialTouchX=0,r.initialTouchY=0)},r.onControlTouchEnd=function(e){r.userIsDragging||r.onControlMouseDown(e)},r.onClearIndicatorTouchEnd=function(e){r.userIsDragging||r.onClearIndicatorMouseDown(e)},r.onDropdownIndicatorTouchEnd=function(e){r.userIsDragging||r.onDropdownIndicatorMouseDown(e)},r.handleInputChange=function(e){var t=e.currentTarget.value;r.setState({inputIsHiddenAfterUpdate:!1}),r.onInputChange(t,{action:"input-change"}),r.props.menuIsOpen||r.onMenuOpen()},r.onInputFocus=function(e){r.props.onFocus&&r.props.onFocus(e),r.setState({inputIsHiddenAfterUpdate:!1,isFocused:!0}),(r.openAfterFocus||r.props.openMenuOnFocus)&&r.openMenu("first"),r.openAfterFocus=!1},r.onInputBlur=function(e){r.menuListRef&&r.menuListRef.contains(document.activeElement)?r.inputRef.focus():(r.props.onBlur&&r.props.onBlur(e),r.onInputChange("",{action:"input-blur"}),r.onMenuClose(),r.setState({focusedValue:null,isFocused:!1}))},r.onOptionHover=function(e){r.blockOptionHover||r.state.focusedOption===e||r.setState({focusedOption:e})},r.shouldHideSelectedOptions=function(){return Mr(r.props)},r.onKeyDown=function(e){var t=r.props,n=t.isMulti,o=t.backspaceRemovesValue,i=t.escapeClearsValue,l=t.inputValue,s=t.isClearable,a=t.isDisabled,c=t.menuIsOpen,u=t.onKeyDown,f=t.tabSelectsValue,d=t.openMenuOnFocus,p=r.state,m=p.focusedOption,b=p.focusedValue,h=p.selectValue;if(!(a||"function"==typeof u&&(u(e),e.defaultPrevented))){switch(r.blockOptionHover=!0,e.key){case"ArrowLeft":if(!n||l)return;r.focusValue("previous");break;case"ArrowRight":if(!n||l)return;r.focusValue("next");break;case"Delete":case"Backspace":if(l)return;if(b)r.removeValue(b);else{if(!o)return;n?r.popValue():s&&r.clearValue()}break;case"Tab":if(r.isComposing)return;if(e.shiftKey||!c||!f||!m||d&&r.isOptionSelected(m,h))return;r.selectOption(m);break;case"Enter":if(229===e.keyCode)break;if(c){if(!m)return;if(r.isComposing)return;r.selectOption(m);break}return;case"Escape":c?(r.setState({inputIsHiddenAfterUpdate:!1}),r.onInputChange("",{action:"menu-close"}),r.onMenuClose()):s&&i&&r.clearValue();break;case" ":if(l)return;if(!c){r.openMenu("first");break}if(!m)return;r.selectOption(m);break;case"ArrowUp":c?r.focusOption("up"):r.openMenu("last");break;case"ArrowDown":c?r.focusOption("down"):r.openMenu("first");break;case"PageUp":if(!c)return;r.focusOption("pageup");break;case"PageDown":if(!c)return;r.focusOption("pagedown");break;case"Home":if(!c)return;r.focusOption("first");break;case"End":if(!c)return;r.focusOption("last");break;default:return}e.preventDefault()}},r.instancePrefix="react-select-"+(r.props.instanceId||++Lr),r.state.selectValue=rn(e.value),r}return zt(n,[{key:"componentDidMount",value:function(){this.startListeningComposition(),this.startListeningToTouch(),this.props.closeMenuOnScroll&&document&&document.addEventListener&&document.addEventListener("scroll",this.onScroll,!0),this.props.autoFocus&&this.focusInput()}},{key:"componentDidUpdate",value:function(e){var t,n,r,o,i,l=this.props,s=l.isDisabled,a=l.menuIsOpen,c=this.state.isFocused;(c&&!s&&e.isDisabled||c&&a&&!e.menuIsOpen)&&this.focusInput(),c&&s&&!e.isDisabled&&this.setState({isFocused:!1},this.onMenuClose),this.menuListRef&&this.focusedOptionRef&&this.scrollToFocusedOptionOnUpdate&&(t=this.menuListRef,n=this.focusedOptionRef,r=t.getBoundingClientRect(),o=n.getBoundingClientRect(),i=n.offsetHeight/3,o.bottom+i>r.bottom?an(t,Math.min(n.offsetTop+n.clientHeight-t.offsetHeight+i,t.scrollHeight)):o.top-i<r.top&&an(t,Math.max(n.offsetTop-i,0)),this.scrollToFocusedOptionOnUpdate=!1)}},{key:"componentWillUnmount",value:function(){this.stopListeningComposition(),this.stopListeningToTouch(),document.removeEventListener("scroll",this.onScroll,!0)}},{key:"onMenuOpen",value:function(){this.props.onMenuOpen()}},{key:"onMenuClose",value:function(){this.onInputChange("",{action:"menu-close"}),this.props.onMenuClose()}},{key:"onInputChange",value:function(e,t){this.props.onInputChange(e,t)}},{key:"focusInput",value:function(){this.inputRef&&this.inputRef.focus()}},{key:"blurInput",value:function(){this.inputRef&&this.inputRef.blur()}},{key:"openMenu",value:function(e){var t=this,n=this.state,r=n.selectValue,o=n.isFocused,i=this.buildFocusableOptions(),l="first"===e?0:i.length-1;if(!this.props.isMulti){var s=i.indexOf(r[0]);s>-1&&(l=s)}this.scrollToFocusedOptionOnUpdate=!(o&&this.menuListRef),this.setState({inputIsHiddenAfterUpdate:!1,focusedValue:null,focusedOption:i[l]},(function(){return t.onMenuOpen()}))}},{key:"focusValue",value:function(e){var t=this.state,n=t.selectValue,r=t.focusedValue;if(this.props.isMulti){this.setState({focusedOption:null});var o=n.indexOf(r);r||(o=-1);var i=n.length-1,l=-1;if(n.length){switch(e){case"previous":l=0===o?0:-1===o?i:o-1;break;case"next":o>-1&&o<i&&(l=o+1)}this.setState({inputIsHidden:-1!==l,focusedValue:n[l]})}}}},{key:"focusOption",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"first",t=this.props.pageSize,n=this.state.focusedOption,r=this.getFocusableOptions();if(r.length){var o=0,i=r.indexOf(n);n||(i=-1),"up"===e?o=i>0?i-1:r.length-1:"down"===e?o=(i+1)%r.length:"pageup"===e?(o=i-t)<0&&(o=0):"pagedown"===e?(o=i+t)>r.length-1&&(o=r.length-1):"last"===e&&(o=r.length-1),this.scrollToFocusedOptionOnUpdate=!0,this.setState({focusedOption:r[o],focusedValue:null})}}},{key:"getTheme",value:function(){return this.props.theme?"function"==typeof this.props.theme?this.props.theme(wr):Xt(Xt({},wr),this.props.theme):wr}},{key:"getCommonProps",value:function(){var e=this.clearValue,t=this.cx,n=this.getStyles,r=this.getValue,o=this.selectOption,i=this.setValue,l=this.props,s=l.isMulti,a=l.isRtl,c=l.options;return{clearValue:e,cx:t,getStyles:n,getValue:r,hasValue:this.hasValue(),isMulti:s,isRtl:a,options:c,selectOption:o,selectProps:l,setValue:i,theme:this.getTheme()}}},{key:"hasValue",value:function(){return this.state.selectValue.length>0}},{key:"hasOptions",value:function(){return!!this.getFocusableOptions().length}},{key:"isClearable",value:function(){var e=this.props,t=e.isClearable,n=e.isMulti;return void 0===t?n:t}},{key:"isOptionDisabled",value:function(e,t){return Dr(this.props,e,t)}},{key:"isOptionSelected",value:function(e,t){return Rr(this.props,e,t)}},{key:"filterOption",value:function(e,t){return Tr(this.props,e,t)}},{key:"formatOptionLabel",value:function(e,t){if("function"==typeof this.props.formatOptionLabel){var n=this.props.inputValue,r=this.state.selectValue;return this.props.formatOptionLabel(e,{context:t,inputValue:n,selectValue:r})}return this.getOptionLabel(e)}},{key:"formatGroupLabel",value:function(e){return this.props.formatGroupLabel(e)}},{key:"startListeningComposition",value:function(){document&&document.addEventListener&&(document.addEventListener("compositionstart",this.onCompositionStart,!1),document.addEventListener("compositionend",this.onCompositionEnd,!1))}},{key:"stopListeningComposition",value:function(){document&&document.removeEventListener&&(document.removeEventListener("compositionstart",this.onCompositionStart),document.removeEventListener("compositionend",this.onCompositionEnd))}},{key:"startListeningToTouch",value:function(){document&&document.addEventListener&&(document.addEventListener("touchstart",this.onTouchStart,!1),document.addEventListener("touchmove",this.onTouchMove,!1),document.addEventListener("touchend",this.onTouchEnd,!1))}},{key:"stopListeningToTouch",value:function(){document&&document.removeEventListener&&(document.removeEventListener("touchstart",this.onTouchStart),document.removeEventListener("touchmove",this.onTouchMove),document.removeEventListener("touchend",this.onTouchEnd))}},{key:"renderInput",value:function(){var e=this.props,t=e.isDisabled,n=e.isSearchable,r=e.inputId,o=e.inputValue,i=e.tabIndex,l=e.form,s=this.getComponents().Input,a=this.state.inputIsHidden,c=this.commonProps,u=r||this.getElementId("input"),f={"aria-autocomplete":"list","aria-label":this.props["aria-label"],"aria-labelledby":this.props["aria-labelledby"]};return n?be.a.createElement(s,fe({},c,{autoCapitalize:"none",autoComplete:"off",autoCorrect:"off",id:u,innerRef:this.getInputRef,isDisabled:t,isHidden:a,onBlur:this.onInputBlur,onChange:this.handleInputChange,onFocus:this.onInputFocus,spellCheck:"false",tabIndex:i,form:l,type:"text",value:o},f)):be.a.createElement(cr,fe({id:u,innerRef:this.getInputRef,onBlur:this.onInputBlur,onChange:en,onFocus:this.onInputFocus,readOnly:!0,disabled:t,tabIndex:i,form:l,value:""},f))}},{key:"renderPlaceholderOrValue",value:function(){var e=this,t=this.getComponents(),n=t.MultiValue,r=t.MultiValueContainer,o=t.MultiValueLabel,i=t.MultiValueRemove,l=t.SingleValue,s=t.Placeholder,a=this.commonProps,c=this.props,u=c.controlShouldRenderValue,f=c.isDisabled,d=c.isMulti,p=c.inputValue,m=c.placeholder,b=this.state,h=b.selectValue,v=b.focusedValue,g=b.isFocused;if(!this.hasValue()||!u)return p?null:be.a.createElement(s,fe({},a,{key:"placeholder",isDisabled:f,isFocused:g}),m);if(d)return h.map((function(t,l){var s=t===v;return be.a.createElement(n,fe({},a,{components:{Container:r,Label:o,Remove:i},isFocused:s,isDisabled:f,key:"".concat(e.getOptionValue(t)).concat(l),index:l,removeProps:{onClick:function(){return e.removeValue(t)},onTouchEnd:function(){return e.removeValue(t)},onMouseDown:function(e){e.preventDefault(),e.stopPropagation()}},data:t}),e.formatOptionLabel(t,"value"))}));if(p)return null;var y=h[0];return be.a.createElement(l,fe({},a,{data:y,isDisabled:f}),this.formatOptionLabel(y,"value"))}},{key:"renderClearIndicator",value:function(){var e=this.getComponents().ClearIndicator,t=this.commonProps,n=this.props,r=n.isDisabled,o=n.isLoading,i=this.state.isFocused;if(!this.isClearable()||!e||r||!this.hasValue()||o)return null;var l={onMouseDown:this.onClearIndicatorMouseDown,onTouchEnd:this.onClearIndicatorTouchEnd,"aria-hidden":"true"};return be.a.createElement(e,fe({},t,{innerProps:l,isFocused:i}))}},{key:"renderLoadingIndicator",value:function(){var e=this.getComponents().LoadingIndicator,t=this.commonProps,n=this.props,r=n.isDisabled,o=n.isLoading,i=this.state.isFocused;return e&&o?be.a.createElement(e,fe({},t,{innerProps:{"aria-hidden":"true"},isDisabled:r,isFocused:i})):null}},{key:"renderIndicatorSeparator",value:function(){var e=this.getComponents(),t=e.DropdownIndicator,n=e.IndicatorSeparator;if(!t||!n)return null;var r=this.commonProps,o=this.props.isDisabled,i=this.state.isFocused;return be.a.createElement(n,fe({},r,{isDisabled:o,isFocused:i}))}},{key:"renderDropdownIndicator",value:function(){var e=this.getComponents().DropdownIndicator;if(!e)return null;var t=this.commonProps,n=this.props.isDisabled,r=this.state.isFocused,o={onMouseDown:this.onDropdownIndicatorMouseDown,onTouchEnd:this.onDropdownIndicatorTouchEnd,"aria-hidden":"true"};return be.a.createElement(e,fe({},t,{innerProps:o,isDisabled:n,isFocused:r}))}},{key:"renderMenu",value:function(){var e=this,t=this.getComponents(),n=t.Group,r=t.GroupHeading,o=t.Menu,i=t.MenuList,l=t.MenuPortal,s=t.LoadingMessage,a=t.NoOptionsMessage,c=t.Option,u=this.commonProps,f=this.state.focusedOption,d=this.props,p=d.captureMenuScroll,m=d.inputValue,b=d.isLoading,h=d.loadingMessage,v=d.minMenuHeight,g=d.maxMenuHeight,y=d.menuIsOpen,O=d.menuPlacement,_=d.menuPosition,j=d.menuPortalTarget,w=d.menuShouldBlockScroll,x=d.menuShouldScrollIntoView,E=d.noOptionsMessage,k=d.onMenuScrollToTop,C=d.onMenuScrollToBottom;if(!y)return null;var S,P=function(t,n){var r=t.type,o=t.data,i=t.isDisabled,l=t.isSelected,s=t.label,a=t.value,d=f===o,p=i?void 0:function(){return e.onOptionHover(o)},m=i?void 0:function(){return e.selectOption(o)},b="".concat(e.getElementId("option"),"-").concat(n),h={id:b,onClick:m,onMouseMove:p,onMouseOver:p,tabIndex:-1};return be.a.createElement(c,fe({},u,{innerProps:h,data:o,isDisabled:i,isSelected:l,key:b,label:s,type:r,value:a,isFocused:d,innerRef:d?e.getFocusedOptionRef:void 0}),e.formatOptionLabel(t.data,"menu"))};if(this.hasOptions())S=this.getCategorizedOptions().map((function(t){if("group"===t.type){var o=t.data,i=t.options,l=t.index,s="".concat(e.getElementId("group"),"-").concat(l),a="".concat(s,"-heading");return be.a.createElement(n,fe({},u,{key:s,data:o,options:i,Heading:r,headingProps:{id:a,data:t.data},label:e.formatGroupLabel(t.data)}),t.options.map((function(e){return P(e,"".concat(l,"-").concat(e.index))})))}if("option"===t.type)return P(t,"".concat(t.index))}));else if(b){var I=h({inputValue:m});if(null===I)return null;S=be.a.createElement(s,u,I)}else{var D=E({inputValue:m});if(null===D)return null;S=be.a.createElement(a,u,D)}var R={minMenuHeight:v,maxMenuHeight:g,menuPlacement:O,menuPosition:_,menuShouldScrollIntoView:x},T=be.a.createElement(yn,fe({},u,R),(function(t){var n=t.ref,r=t.placerProps,l=r.placement,s=r.maxHeight;return be.a.createElement(o,fe({},u,R,{innerRef:n,innerProps:{onMouseDown:e.onMenuMouseDown,onMouseMove:e.onMenuMouseMove},isLoading:b,placement:l}),be.a.createElement(_r,{captureEnabled:p,onTopArrive:k,onBottomArrive:C,lockEnabled:w},(function(t){return be.a.createElement(i,fe({},u,{innerRef:function(n){e.getMenuListRef(n),t(n)},isLoading:b,maxHeight:s,focusedOption:f}),S)})))}));return j||"fixed"===_?be.a.createElement(l,fe({},u,{appendTo:j,controlElement:this.controlRef,menuPlacement:O,menuPosition:_}),T):T}},{key:"renderFormField",value:function(){var e=this,t=this.props,n=t.delimiter,r=t.isDisabled,o=t.isMulti,i=t.name,l=this.state.selectValue;if(i&&!r){if(o){if(n){var s=l.map((function(t){return e.getOptionValue(t)})).join(n);return be.a.createElement("input",{name:i,type:"hidden",value:s})}var a=l.length>0?l.map((function(t,n){return be.a.createElement("input",{key:"i-".concat(n),name:i,type:"hidden",value:e.getOptionValue(t)})})):be.a.createElement("input",{name:i,type:"hidden"});return be.a.createElement("div",null,a)}var c=l[0]?this.getOptionValue(l[0]):"";return be.a.createElement("input",{name:i,type:"hidden",value:c})}}},{key:"renderLiveRegion",value:function(){var e=this.commonProps,t=this.state,n=t.ariaSelection,r=t.focusedOption,o=t.focusedValue,i=t.isFocused,l=t.selectValue,s=this.getFocusableOptions();return be.a.createElement(Jn,fe({},e,{ariaSelection:n,focusedOption:r,focusedValue:o,isFocused:i,selectValue:l,focusableOptions:s}))}},{key:"render",value:function(){var e=this.getComponents(),t=e.Control,n=e.IndicatorsContainer,r=e.SelectContainer,o=e.ValueContainer,i=this.props,l=i.className,s=i.id,a=i.isDisabled,c=i.menuIsOpen,u=this.state.isFocused,f=this.commonProps=this.getCommonProps();return be.a.createElement(r,fe({},f,{className:l,innerProps:{id:s,onKeyDown:this.onKeyDown},isDisabled:a,isFocused:u}),this.renderLiveRegion(),be.a.createElement(t,fe({},f,{innerRef:this.getControlRef,innerProps:{onMouseDown:this.onControlMouseDown,onTouchEnd:this.onControlTouchEnd},isDisabled:a,isFocused:u,menuIsOpen:c}),be.a.createElement(o,fe({},f,{isDisabled:a}),this.renderPlaceholderOrValue(),this.renderInput()),be.a.createElement(n,fe({},f,{isDisabled:a}),this.renderClearIndicator(),this.renderLoadingIndicator(),this.renderIndicatorSeparator(),this.renderDropdownIndicator())),this.renderMenu(),this.renderFormField())}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n=t.prevProps,r=t.clearFocusValueOnUpdate,o=t.inputIsHiddenAfterUpdate,i=e.options,l=e.value,s=e.menuIsOpen,a=e.inputValue,c={};if(n&&(l!==n.value||i!==n.options||s!==n.menuIsOpen||a!==n.inputValue)){var u=rn(l),f=s?function(e,t){return Cr(kr(e,t))}(e,u):[],d=r?function(e,t){var n=e.focusedValue,r=e.selectValue.indexOf(n);if(r>-1){if(t.indexOf(n)>-1)return n;if(r<t.length)return t[r]}return null}(t,u):null;c={selectValue:u,focusedOption:function(e,t){var n=e.focusedOption;return n&&t.indexOf(n)>-1?n:t[0]}(t,f),focusedValue:d,clearFocusValueOnUpdate:!1}}var p=null!=o&&e!==n?{inputIsHidden:o,inputIsHiddenAfterUpdate:void 0}:{};return Xt(Xt(Xt({},c),p),{},{prevProps:e})}}]),n}(me.Component);Ar.defaultProps=xr;var Nr,Fr,Br,Vr=(n(50),n(31),n(54),{cacheOptions:!1,defaultOptions:!1,filterOption:null,isLoading:!1}),Ur=function(e){var t,n;return n=t=function(t){Wt(r,t);var n=Zt(r);function r(e){var t;return Ht(this,r),(t=n.call(this)).select=void 0,t.lastRequest=void 0,t.mounted=!1,t.handleInputChange=function(e,n){var r=t.props,o=r.cacheOptions,i=function(e,t,n){if(n){var r=n(e,t);if("string"==typeof r)return r}return e}(e,n,r.onInputChange);if(!i)return delete t.lastRequest,void t.setState({inputValue:"",loadedInputValue:"",loadedOptions:[],isLoading:!1,passEmptyOptions:!1});if(o&&t.state.optionsCache[i])t.setState({inputValue:i,loadedInputValue:i,loadedOptions:t.state.optionsCache[i],isLoading:!1,passEmptyOptions:!1});else{var l=t.lastRequest={};t.setState({inputValue:i,isLoading:!0,passEmptyOptions:!t.state.loadedInputValue},(function(){t.loadOptions(i,(function(e){t.mounted&&l===t.lastRequest&&(delete t.lastRequest,t.setState((function(t){return{isLoading:!1,loadedInputValue:i,loadedOptions:e||[],passEmptyOptions:!1,optionsCache:e?Xt(Xt({},t.optionsCache),{},Object(pe.a)({},i,e)):t.optionsCache}})))}))}))}return i},t.state={defaultOptions:Array.isArray(e.defaultOptions)?e.defaultOptions:void 0,inputValue:void 0!==e.inputValue?e.inputValue:"",isLoading:!0===e.defaultOptions,loadedOptions:[],passEmptyOptions:!1,optionsCache:{},prevDefaultOptions:void 0,prevCacheOptions:void 0},t}return zt(r,[{key:"componentDidMount",value:function(){var e=this;this.mounted=!0;var t=this.props.defaultOptions,n=this.state.inputValue;!0===t&&this.loadOptions(n,(function(t){if(e.mounted){var n=!!e.lastRequest;e.setState({defaultOptions:t||[],isLoading:n})}}))}},{key:"componentWillUnmount",value:function(){this.mounted=!1}},{key:"focus",value:function(){this.select.focus()}},{key:"blur",value:function(){this.select.blur()}},{key:"loadOptions",value:function(e,t){var n=this.props.loadOptions;if(!n)return t();var r=n(e,t);r&&"function"==typeof r.then&&r.then(t,(function(){return t()}))}},{key:"render",value:function(){var t=this,n=this.props;n.loadOptions;var r=n.isLoading,o=de(n,["loadOptions","isLoading"]),i=this.state,l=i.defaultOptions,s=i.inputValue,a=i.isLoading,c=i.loadedInputValue,u=i.loadedOptions,f=i.passEmptyOptions?[]:s&&c?u:l||[];return be.a.createElement(e,fe({},o,{ref:function(e){t.select=e},options:f,isLoading:a||r,onInputChange:this.handleInputChange}))}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n=e.cacheOptions!==t.prevCacheOptions?{prevCacheOptions:e.cacheOptions,optionsCache:{}}:{},r=e.defaultOptions!==t.prevDefaultOptions?{prevDefaultOptions:e.defaultOptions,defaultOptions:Array.isArray(e.defaultOptions)?e.defaultOptions:void 0}:{};return Xt(Xt({},n),r)}}]),r}(me.Component),t.defaultProps=Vr,n}((Nr=Ar,Br=Fr=function(e){Wt(n,e);var t=Zt(n);function n(){var e;Ht(this,n);for(var r=arguments.length,o=new Array(r),i=0;i<r;i++)o[i]=arguments[i];return(e=t.call.apply(t,[this].concat(o))).select=void 0,e.state={inputValue:void 0!==e.props.inputValue?e.props.inputValue:e.props.defaultInputValue,menuIsOpen:void 0!==e.props.menuIsOpen?e.props.menuIsOpen:e.props.defaultMenuIsOpen,value:void 0!==e.props.value?e.props.value:e.props.defaultValue},e.onChange=function(t,n){e.callProp("onChange",t,n),e.setState({value:t})},e.onInputChange=function(t,n){var r=e.callProp("onInputChange",t,n);e.setState({inputValue:void 0!==r?r:t})},e.onMenuOpen=function(){e.callProp("onMenuOpen"),e.setState({menuIsOpen:!0})},e.onMenuClose=function(){e.callProp("onMenuClose"),e.setState({menuIsOpen:!1})},e}return zt(n,[{key:"focus",value:function(){this.select.focus()}},{key:"blur",value:function(){this.select.blur()}},{key:"getProp",value:function(e){return void 0!==this.props[e]?this.props[e]:this.state[e]}},{key:"callProp",value:function(e){if("function"==typeof this.props[e]){for(var t,n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return(t=this.props)[e].apply(t,r)}}},{key:"render",value:function(){var e=this,t=this.props;t.defaultInputValue,t.defaultMenuIsOpen,t.defaultValue;var n=de(t,["defaultInputValue","defaultMenuIsOpen","defaultValue"]);return be.a.createElement(Nr,fe({},n,{ref:function(t){e.select=t},inputValue:this.getProp("inputValue"),menuIsOpen:this.getProp("menuIsOpen"),onChange:this.onChange,onInputChange:this.onInputChange,onMenuClose:this.onMenuClose,onMenuOpen:this.onMenuOpen,value:this.getProp("value")}))}}]),n}(me.Component),Fr.defaultProps={defaultInputValue:"",defaultMenuIsOpen:!1,defaultValue:null},Br)),Hr=n(6);function qr(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function zr(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?qr(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):qr(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}n(60);var $r=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o);for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return e=r.call.apply(r,[this].concat(n)),U()(ce()(e),"selectRef",null),U()(ce()(e),"getDefaultClassName",(function(){return"llms-search"})),U()(ce()(e),"getSearchPath",(function(){return e.props.searchPath})),U()(ce()(e),"getSearchUrl",(function(t){return wp.url.addQueryArgs(e.getSearchPath(),e.getSearchArgs(t))})),U()(ce()(e),"formatSearchResultLabel",(function(e){return e.id})),U()(ce()(e),"formatSearchResultValue",(function(e){return e.id})),U()(ce()(e),"onSearch",Object(ue.debounce)(300,(function(t,n){wp.apiFetch({path:e.getSearchUrl(t)}).then((function(t){n(e.formatSearchResults(t))}))}))),e}return J()(o,[{key:"getSearchArgs",value:function(e){return Object.assign({per_page:20,search:encodeURI(e)},this.props.searchArgs)}},{key:"formatSearchResults",value:function(e){var t=this;return e.map((function(e){return zr(zr({},e),{},{label:t.formatSearchResultLabel(e),value:t.formatSearchResultValue(e)})}))}},{key:"render",value:function(){var e=this,t=this.props,n=t.label,r=t.isMulti,o=t.isDisabled,i=t.onChange,l=t.placeholder,s=t.selected,a=this.props.className||this.getDefaultClassName();return Object(z.createElement)(K.BaseControl,{id:Object(Hr.uniqueId)("".concat(a,"--")),label:n},Object(z.createElement)(Ur,{ref:function(t){return e.selectRef=t},className:a,classNamePrefix:"llms-search",isMulti:r,isDisabled:o,value:this.formatSearchResults(s||[]),defaultOptions:s,placeholder:l,loadOptions:this.onSearch,onChange:i,styles:{control:function(e){return zr(zr({},e),{},{borderColor:"#8d96a0","&:hover":zr(zr({},e["&:hover"]),{},{borderColor:"#8d96a0"})})}},theme:function(e){return zr(zr({},e),{},{colors:zr(zr({},e.colors),{},{primary:"#008dbe",primary25:"#ccf2ff",primary50:"#b3ecff",primary75:"#4dd2ff"}),spacing:zr(zr({},e.spacing),{},{baseUnit:2,controlHeight:28,menuGutter:4})})}}))}}]),o}(z.Component);var Wr=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o);for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return e=r.call.apply(r,[this].concat(n)),U()(ce()(e),"getDefaultClassName",(function(){return"llms-search--".concat(e.props.postType.replace("llms_",""))})),U()(ce()(e),"getSearchPath",(function(){return e.props.searchPath||"/wp/v2/".concat(e.props.postType)})),U()(ce()(e),"formatSearchResultLabel",(function(e){return Object($.sprintf)(// Translators: %1$s = Post title; %2$ = post id. +Object($._x)("%1$s (ID# %2$d)","Search result label","lifterlms"),e.title.rendered,e.id)})),e}return o}($r),Gr=Object(W.createHigherOrderComponent)((function(e){return function(t){if(!q(wp.blocks.getBlockType(t.name),t.name))return Object(z.createElement)(e,t);var n=t.attributes,r=n.llms_visibility,o=n.llms_visibility_in,i=t.setAttributes;if(!r||"off"===r)return Object(z.createElement)(e,t);var l=t.attributes.llms_visibility_posts;void 0===l&&(l="[]"),l=JSON.parse(l);var s,a=function(){var e=wp.data.select("core/editor").getCurrentPost(),t=[];return-1!==["course","lesson"].indexOf(e.type)&&t.push({value:"this",label:Object($.__)("in this course","lifterlms")}),t.push({value:"any_course",label:Object($.__)("in any course","lifterlms")}),-1!==["llms_membership"].indexOf(e.type)&&t.push({value:"this",label:Object($.__)("in this membership","lifterlms")}),t.push({value:"any_membership",label:Object($.__)("in any membership","lifterlms")},{value:"any",label:Object($.__)("in any course or membership","lifterlms")},{value:"list_all",label:Object($.__)("in all of the selected courses or memberships","lifterlms")},{value:"list_any",label:Object($.__)("in any of the selected courses or memberships","lifterlms")}),Object(H.applyFilters)("llms_blocks_block_visibility_in_options",t,e)},c=function(e,t){"select-option"===t.action?u(t.option):"remove-value"===t.action&&f(t.removedValue)},u=function(e){l.map((function(e){return e.id})).includes(e.id)||l.push(e),d()},f=function(e){l.splice(l.map((function(e){return e.id})).indexOf(e.id),1),d()},d=function(){var e=l.map((function(e){var t={id:e.id,title:e.title,type:e.type};return Object(H.applyFilters)("llms_block_visibility_stored_post_props",t,e)}));i({llms_visibility_posts:JSON.stringify(e)})};return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(se,t,Object(z.createElement)(e,t)),Object(z.createElement)(G.InspectorControls,null,Object(z.createElement)(K.PanelBody,{title:Object($.__)("Enrollment Visibility","lifterlms")},Object(z.createElement)(K.SelectControl,{className:"llms-visibility-select",label:Object($.__)("Display to","lifterlms"),value:r,onChange:function(e){i({llms_visibility:e,llms_visibility_in:a()[0].value})},options:Object(H.applyFilters)("llms_block_visibility_settings_options",le)}),-1===["all","logged_in","logged_out"].indexOf(r)&&Object(z.createElement)(z.Fragment,null,Object(z.createElement)(K.SelectControl,{className:"llms-visibility-select--in",label:(s=r,"enrolled"===s?Object($.__)("Enrolled In","lifterlms"):Object($.__)("Not Enrolled In","lifterlms")),value:o,onChange:function(e){return i({llms_visibility_in:e})},options:a()}),("list_all"===o||"list_any"===o)&&Object(z.createElement)("div",null,Object(z.createElement)(Wr,{isMulti:!0,postType:"course",label:Object($.__)("Courses","lifterlms"),placeholder:Object($.__)("Search by course title…","lifterlms"),onChange:c,selected:l.filter((function(e){return"course"===e.type}))}),Object(z.createElement)(Wr,{isMulti:!0,postType:"llms_membership",label:Object($.__)("Memberships","lifterlms"),placeholder:Object($.__)("Search by membership title…","lifterlms"),onChange:c,selected:l.filter((function(e){return"llms_membership"===e.type}))}))))))}}),"withInspectorControl");Object(H.addFilter)("blocks.registerBlockType","llms/visibility-attributes",(function(e,t){if(!q(e,t))return e;e.attributes||(e.attributes={});var n={llms_visibility:{default:"all",type:"string"},llms_visibility_in:{default:"",type:"string"},llms_visibility_posts:{default:"[]",type:"string"}};return Object.keys(n).forEach((function(t){var r,o,i;e.attributes=(r=e.attributes,i=n[t],r[o=t]&&r[o].default?r[o].type=i.type:r[o]=i,r)})),e})),Object(H.addFilter)("editor.BlockEdit","llms/visibility-controls",Gr);var Kr=n(35),Yr=n.n(Kr),Xr=n(4),Qr=n(18),Jr=n(10),Zr=function(){return function e(t){var n=[];return t.forEach((function(t){if("core/block"===t.name){var r=Object(Xr.select)(G.store).getBlocks;n=n.concat(e(r(t.clientId)))}else t.innerBlocks.length?n=n.concat(e(t.innerBlocks)):n.push(t)})),n}((0,Object(Xr.select)(G.store).getBlocks)())},eo=function(){return!!(window.llms&&window.llms.post&&window.llms.post.post_type)&&window.llms.post.post_type};var to=function(){var e,t,n,r,o,i,l,s,a,c;e=Object(Xr.select)(Qr.store).getEditedPostAttribute("meta")._llms_form_location,["registration","account"].includes(e)&&Object(H.addFilter)("llms_block_supports_visibility","llms/form-block-visibility",(function(){return!1})),a={"llms/form-field-user-email":["all","logged_out"],"llms/form-field-user-password":["all","logged_out"],"llms/form-field-user-login":["logged_out"]},c=Object.keys(a),Object(H.addFilter)("llms_block_visibility_settings_options","llms/form-block-visibility-options",(function(e){var t,n,r,o=(0,Object(Xr.select)(G.store).getSelectedBlock)();return o&&(n=(t=o).name,r=t.innerBlocks,"llms/form-field-confirm-group"===n?Object(Hr.some)(r,(function(e){return c.includes(e.name)})):c.includes(n))?e.filter((function(e){var t=e.value;return function(e){var t=e.name,n=e.innerBlocks,r=t;if("llms/form-field-confirm-group"===t){var o=n.find((function(e){return c.includes(e.name)}));r=o?o.name:r}return a[r]||[]}(o).includes(t)})):e})),t=Object(Xr.select)(Qr.store).getEditedPostAttribute("meta")._llms_form_is_core,n=[".edit-post-layout .components-panel__body.edit-post-post-status"],"yes"===t&&n.push(".edit-post-layout button.editor-post-switch-to-draft"),Object(Xr.subscribe)((function(){setTimeout((function(){document.querySelectorAll(n.join(",")).forEach((function(e){e.style.display="none"}))}),1)})),r="llms-forms-no-email-error-notice",o=Object(Xr.select)("core/notices").getNotices,i=Object(Xr.dispatch)("core/notices"),l=i.createErrorNotice,s=i.removeNotice,Object(Xr.subscribe)(Object(Hr.debounce)((function(){var e=Object(Xr.select)("core/editor").getCurrentPost(),t=Zr().map((function(e){return e.name}));if(e.content.includes("\x3c!-- wp:")&&t.length){var n=o().map((function(e){return e.id})).includes(r),i=document.querySelector("button.editor-post-publish-button");t.includes("llms/form-field-user-email")||n?t.includes("llms/form-field-user-email")&&n&&(s(r),i.disabled=!1):(l(Object($.__)("User Email is a required field.","lifterlms"),{id:r,isDismissible:!1,actions:[{label:Object($.__)("Restore user email field?","lifterlms"),onClick:function(){(Object(Xr.dispatch)("core/block-editor")||Object(Xr.dispatch)("core/editor")).insertBlock(Object(Jr.createBlock)("llms/form-field-user-email"),0)}}]}),i.disabled=!0)}}),500))};function no(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function ro(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?no(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):no(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function oo(e){return e.reduce((function(e,t){return ro(ro({},e),{},U()({},t.name,t))}),{})}function io(e){return Object.values(e)}function lo(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function so(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?lo(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):lo(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var ao=oo(window.llms.userInfoFields.map((function(e){return so(so({},e),{},{isPersisted:!0})})));function co(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function uo(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?co(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):co(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function fo(e,t){return uo(uo({},e),{},U()({},t.name,uo({},t)))}function po(e,t){return oo(e=io(e).filter((function(e){return e.name!==t})))}function mo(e,t,n){return uo(uo({},e),{},U()({},t,uo(uo({},e[t]),n)))}function bo(e,t,n){var r=uo({},e[t]);return fo(e=po(e,t),uo(uo({},r),{},{name:n}))}function ho(){return ao}function vo(e){return{type:"ADD_FIELD",field:e}}function go(e){return{type:"DELETE_FIELD",name:e}}function yo(e,t){return{type:"EDIT_FIELD",name:e,edits:t}}function Oo(e,t){return{type:"EDIT_FIELD",name:e,edits:{clientId:t}}}function _o(e){return{type:"EDIT_FIELD",name:e,edits:{clientId:null}}}function jo(e){return{type:"RECEIVE_FIELDS",fields:e}}function wo(e,t){return{type:"RENAME_FIELD",oldName:e,newName:t}}function xo(){return{type:"RESET_FIELDS"}}function Eo(e,t){return!!e.fields[t]}function ko(e,t){return e.fields[t]||null}function Co(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"global",o="global"===r?e.fields:Po(e);return io(o).find((function(e){return e[n]===t}))||null}function So(e){return e.fields}function Po(e){return oo(io(e.fields).filter((function(e){return e.clientId})))}function Io(e,t,n){var r=ko(e,t);return!(!r||!r.clientId||r.clientId===n)}function Do(e,t){return!!Co(e,t,"clientId","local")}function Ro(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function To(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Ro(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Ro(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var Mo={reducer:Object(Xr.combineReducers)({fields:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ao,t=arguments.length>1?arguments[1]:void 0,n=t.type;switch(n){case"ADD_FIELD":return fo(e,t.field);case"DELETE_FIELD":return po(e,t.name);case"EDIT_FIELD":return mo(e,t.name,t.edits);case"RECEIVE_FIELDS":return oo(t.fields);case"RENAME_FIELD":return bo(e,t.oldName,t.newName);case"RESET_FIELDS":return ho();default:return e}}}),actions:To({},r),selectors:To({},o)},Lo=Object(Xr.createReduxStore)("llms/user-info-fields",Mo);Object(Xr.register)(Lo);var Ao=[],No=function(e,t){return Object(Hr.differenceBy)(e,t,"clientId").filter((function(e){return 0===e.name.indexOf("llms/form-")}))};function Fo(){var e=Zr(),t=No(Ao,e),n=No(e,Ao);Ao=e,function(e){e.forEach((function(e){var t=e.attributes.name,n=Object(Xr.select)(Lo).getField,r=Object(Xr.dispatch)(Lo),o=r.deleteField,i=r.unloadField,l=n(t);l&&(l.isPersisted?i(t):o(t))}))}(t),setTimeout((function(){!function(e){var t=Object(Xr.select)(Lo).fieldExists,n=Object(Xr.dispatch)(Lo),r=n.loadField,o=n.addField;e.forEach((function(e){var n=e.attributes,i=e.clientId,l=n.name;t(l)?r(l,i):o({name:l,clientId:i,id:n.id,label:n.label,data_store:n.data_store,data_store_key:n.data_store_key})}))}(n)}),100)}var Bo=function(){Object(Xr.subscribe)(Fo)},Vo="llms/course-continue-button",Uo=["course"],Ho={title:Object($.__)("Course Continue Button","lifterlms"),icon:{foreground:"#2295ff",src:"migrate"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],edit:function(e){return Object(z.createElement)("div",{className:e.className},Object(z.createElement)("p",{style:{textAlign:"center"}},Object(z.createElement)(K.Button,{isPrimary:!0,isLarge:!0},Object($.__)("Continue","lifterlms"))))},save:function(e){return Object(z.createElement)("div",{className:e.className,style:{textAlign:"center"}},"[lifterlms_course_continue_button]")}};n(61);var qo=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){return X()(this,o),r.apply(this,arguments)}return J()(o,[{key:"render",value:function(){var e=this.props,t=e.attributes,n=t.length,r=t.show_cats,o=t.show_difficulty,i=t.show_length,l=t.show_tags,s=t.show_tracks,a=t.title_size,c=e.setAttributes;return Object(z.createElement)(G.InspectorControls,null,Object(z.createElement)(K.PanelBody,{title:Object($.__)("Course Information Options","lifterlms")},Object(z.createElement)(K.SelectControl,{label:Object($.__)("Title Headline Size","lifterlms"),value:a,onChange:function(e){return c({title_size:e})},help:Object($.__)("Headline size for the information title element.","lifterlms"),options:[{value:"h1",label:"h1"},{value:"h2",label:"h2"},{value:"h3",label:"h3"},{value:"h4",label:"h4"},{value:"h5",label:"h5"},{value:"h6",label:"h6"}]}),Object(z.createElement)(K.TextControl,{label:Object($.__)("Estimated Completion Time","lifterlms"),value:n,onChange:function(e){return c({length:e})},help:Object($.__)("How many hours, days, weeks, etc… should a student expect to spend in order to complete this course.","lifterlms")}),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Estimated Time","lifterlms"),checked:!!i,onChange:function(){return c({show_length:!i})},help:i?Object($.__)("Displaying estimated time","lifterlms"):Object($.__)("Hiding estimated time","lifterlms")}),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Difficulty","lifterlms"),checked:!!o,onChange:function(){return c({show_difficulty:!o})},help:o?Object($.__)("Displaying difficulty","lifterlms"):Object($.__)("Hiding difficulty","lifterlms")}),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Tracks","lifterlms"),checked:!!s,onChange:function(){return c({show_tracks:!s})},help:s?Object($.__)("Displaying tracks list","lifterlms"):Object($.__)("Hiding tracks list","lifterlms")}),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Categories","lifterlms"),checked:!!r,onChange:function(){return c({show_cats:!r})},help:r?Object($.__)("Displaying categories list","lifterlms"):Object($.__)("Hiding categories list","lifterlms")}),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Tags","lifterlms"),checked:!!l,onChange:function(){return c({show_tags:!l})},help:l?Object($.__)("Displaying tags list","lifterlms"):Object($.__)("Hiding tags list","lifterlms")})))}}]),o}(z.Component);var zo=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o);for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return e=r.call.apply(r,[this].concat(n)),U()(ce()(e),"state",{terms:!1}),e}return J()(o,[{key:"getTerms",value:function(){var e=this,t=this.props,n=t.currentPost,r=t.taxonomy,o=n._links["wp:term"].filter((function(e){return e.taxonomy===r}))[0].href;wp.apiFetch({url:wp.url.addQueryArgs(o,{per_page:-1})}).then((function(t){e.setState({terms:t})}))}},{key:"componentDidUpdate",value:function(e){e.currentPost[this.props.taxonomy]!==this.props.currentPost[this.props.taxonomy]&&this.getTerms()}},{key:"componentWillMount",value:function(){this.getTerms()}},{key:"renderTerms",value:function(e){var t=this,n=e.length-1;return Object(z.createElement)(z.Fragment,null,e?e.map((function(e,r){return t.renderTerm(e,n===r)})):Object($.__)("Loading…","lifterlms"))}},{key:"renderTerm",value:function(e,t){return Object(z.createElement)(z.Fragment,null,Object(z.createElement)("a",{href:e.link,target:"_blank",rel:"noreferrer"},e.name),t?"":", ")}},{key:"render",value:function(){var e=this.state.terms,t=this.props.taxonomyName;return Array.isArray(e)&&!e.length?"":Object(z.createElement)("li",null,Object(z.createElement)("strong",null,t),": ",this.renderTerms(e))}}]),o}(z.Component),$o="llms/course-information",Wo=["course"],Go={title:Object($.__)("Course Information","lifterlms"),icon:{foreground:"#2295ff",src:"list-view"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],attributes:{title:{type:"string",default:Object($.__)("Course Information","lifterlms")},title_size:{type:"string",default:"h3"},length:{type:"string",default:"",source:"meta",meta:"_llms_length"},show_cats:{type:"boolean",default:!0},show_difficulty:{type:"boolean",default:!0},show_length:{type:"boolean",default:!0},show_tags:{type:"boolean",default:!0},show_tracks:{type:"boolean",default:!0}},supports:{multiple:!1},edit:function(e){var t=e.attributes,n=e.setAttributes,r=t.length,o=t.show_cats,i=t.show_difficulty,l=t.show_length,s=t.show_tags,a=t.show_tracks,c=t.title,u=t.title_size,f=wp.data.select("core/editor").getCurrentPost(),d=l||i||a||o||s;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(qo,{attributes:t,setAttributes:n}),Object(z.createElement)("div",{className:e.className},Object(z.createElement)(G.RichText,{tagName:u,value:c,onChange:function(e){return n({title:e})}}),d&&Object(z.createElement)(z.Fragment,null,Object(z.createElement)("ul",null,l&&r&&Object(z.createElement)("li",null,Object(z.createElement)("strong",null,Object($.__)("Estimated Time","lifterlms")),": ",r),i&&Object(z.createElement)(zo,{currentPost:f,taxonomy:"course_difficulty",taxonomyName:Object($.__)("Difficulty","lifterlms")}),a&&Object(z.createElement)(zo,{currentPost:f,taxonomy:"course_track",taxonomyName:Object($.__)("Tracks","lifterlms")}),o&&Object(z.createElement)(zo,{currentPost:f,taxonomy:"course_cat",taxonomyName:Object($.__)("Categories","lifterlms")}),s&&Object(z.createElement)(zo,{currentPost:f,taxonomy:"course_tag",taxonomyName:Object($.__)("Tags","lifterlms")})))))},save:function(){return null}},Ko=(n(62),["course"]),Yo="llms/course-progress",Xo={title:Object($.__)("Course Progress","lifterlms"),icon:{foreground:"#2295ff",src:"chart-area"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],supports:{llms_visibility:!1},edit:function(e){return Object(z.createElement)("div",{className:e.className},Object(z.createElement)("div",{className:"progress-bar",value:"50",max:"100"},Object(z.createElement)("div",{className:"progress--fill"})),Object(z.createElement)("span",null,"50%"))},save:function(){return null},deprecated:[{save:function(e){return Object(z.createElement)("div",{className:e.className},"[lifterlms_course_progress]")}}]},Qo=n(20),Jo=n.n(Qo),Zo=(n(63),"llms/course-syllabus"),ei=["course"],ti={title:Object($.__)("Course Syllabus","lifterlms"),icon:{foreground:"#2295ff",src:"grid-view"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],attributes:{course_id:{type:"int",default:0}},edit:function(e){var t=wp.data.select("core/editor").getCurrentPost(),n=e.attributes;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(Jo.a,{block:Zo,attributes:n,urlQueryArgs:{post_id:t.id}}))},save:function(){return null}};var ni=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;return X()(this,o),e=r.apply(this,arguments),U()(ce()(e),"render",(function(){var t=e.props,n=t.name,r=t.attributes,o=t.post_id;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(Jo.a,{block:n,attributes:r,urlQueryArgs:{post_id:o}}))})),e.state={instructors:e.props.instructors},e}return o}(z.Component),ri=Object(W.compose)([Object(Xr.withSelect)((function(e){var t=e("core/editor"),n=t.getEditedPostAttribute;return{post_id:(0,t.getCurrentPostId)(),instructors:n("instructors")}}))])(ni),oi=(n(64),"llms/instructors"),ii=["course","llms_membership"],li={title:Object($.__)("Instructors","lifterlms"),icon:{foreground:"#2295ff",src:"groups"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms"),Object($.__)("Course","lifterlms"),Object($.__)("Memebership","lifterlms")],attributes:{post_id:{type:"int",default:0}},edit:ri,save:function(){return null}},si="llms/lesson-navigation",ai=["lesson"],ci={title:Object($.__)("Lesson Navigation","lifterlms"),icon:{foreground:"#2295ff",src:"leftright"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],edit:function(e){var t=wp.data.select("core/editor").getCurrentPost(),n=e.attributes;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(Jo.a,{block:si,attributes:n,urlQueryArgs:{post_id:t.id}}))},save:function(){return null}},ui=(n(65),"llms/lesson-progression"),fi=["lesson"],di={title:Object($.__)("Lesson Progression (Mark Complete)","lifterlms"),icon:{foreground:"#2295ff",src:"yes"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],supports:{llms_visibility:!1},edit:function(){var e=1*Object(Xr.select)("core/editor").getCurrentPost().meta._llms_quiz,t=!e;return t=Object(H.applyFilters)("llms.lessonProgressBlock.showMainBtn",t),Object(z.createElement)(z.Fragment,null,!!e&&Object(z.createElement)(K.Button,{className:"llms-prog-btn--quiz",isPrimary:!0},Object($.__)("Take Quiz","lifterlms")),t&&Object(z.createElement)(K.Button,{className:"llms-prog-btn--complete",isPrimary:!0},Object($.__)("Mark Complete","lifterlms")))},save:function(){return null}},pi=n(26),mi=n.n(pi),bi=(n(66),null);Object(Xr.subscribe)((function(){var e=Object(Xr.select)("core/editor"),t=e.getCurrentPostLastRevisionId,n=e.isCurrentPostPublished,r=e.isSavingPost,o=e.isPublishingPost;if(n()){var i=mi()("#llms-save-access-plans");i.length&&bi!==t()&&"disabled"!==i.attr("disabled")&&(r()||o())&&(bi=t(),i.trigger("click"))}})),mi()(document).on("llms-access-plan-validation-errors",(function(){Object(Xr.dispatch)("core/notices").createErrorNotice(Object($.__)("Validation errors were encountered while attempting to save your access plans.","lifterlms"),{id:"llms-access-plan-error-notice"})}));var hi="llms/pricing-table",vi=["course","llms_membership"],gi={title:Object($.__)("LifterLMS Pricing Table","lifterlms"),icon:{foreground:"#2295ff",src:"cart"},category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],attributes:{post_id:{type:"int",default:0}},edit:function(e){var t=e.attributes;return mi()(document).one("llms-access-plans-updated",(function(){Object(Xr.dispatch)("core/editor").replaceBlock(e.clientId,Object(Jr.createBlock)(hi)),setTimeout((function(){Object(Xr.dispatch)("core/editor").savePost()}),500)})),Object(z.createElement)(z.Fragment,null,Object(z.createElement)(Jo.a,{block:hi,attributes:t,urlQueryArgs:{post_id:Object(Xr.select)("core/editor").getCurrentPostId()}}))},save:function(){return null}},yi="llms/php-template",Oi={title:Object($.__)("LifterLMS PHP Template","lifterlms"),category:"llms-blocks",keywords:[Object($.__)("LifterLMS","lifterlms")],attributes:{template:{type:"string",default:""},title:{type:"string",default:""}},supports:{html:!1,multiple:!1,reusable:!1,inserter:!1},edit:function(e){var t=e.attributes,n=t.template,r=Object(G.useBlockProps)(),o=t.title;if(!o){var i=window.llmsBlockTemplatesL10n;o=i&&i[n]?i[n]:n}return Object(z.createElement)("div",r,Object(z.createElement)(K.Placeholder,{label:o,className:"wp-block-liftelrms-php-template__placeholder"},Object(z.createElement)("div",{className:"wp-block-liftelrms-php-template__placeholder-copy"},Object(z.createElement)("p",{className:"wp-block-liftelrms-php-template__placeholder-warning"},Object(z.createElement)("strong",null,Object($.__)("Attention: Do not remove this block!","lifterlms"))," ",Object($.__)("Removal will cause unintended effects on your LMS site.","lifterlms")),Object(z.createElement)("p",null,Object($.sprintf)( +/* translators: %s is the template title */ +Object($.__)("This is an editor placeholder for the %s. On your site this will be replaced by the relevant template. You can move this placeholder around and add further blocks around it to extend the template.","lifterlms"),o)))))},save:function(){return null}};function _i(e){return e?e.innerBlocks.length?ji(e.innerBlocks):"core/block"===e.name?ji(Object(Xr.select)("core").getEditedEntityRecord("postType","wp_block",e.attributes.ref).blocks):-1===e.name.indexOf("llms/form-field")?[]:[e]:[e]}function ji(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=[];return Object(Hr.forEach)(e,(function(e){var n=_i(e);n.length&&(t=t.concat(n))})),t}var wi=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"global",r=Object(Xr.select)(Lo),o=r.getFieldBy;return!o(e,t,n)};if("wp_block"===eo()){var xi="";Object(Xr.subscribe)((function(){var e=Object(Xr.select)("core/editor").getEditedPostContent();if(void 0!==e&&e!==xi){xi=e;var t=e.includes("\x3c!-- wp:llms/form-field")?"yes":"no";Object(Xr.dispatch)("core/editor").editPost({is_llms_field:t})}}))}Object(H.addFilter)("blocks.getSaveElement","llms/core-block/save",(function(e,t,n){if("core/block"!==t.name)return e;var r=n.ref;if(Object(Xr.select)("core").hasFinishedResolution("getEntityRecord",["postType","wp_block",r])){var o=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];return Array.isArray(e)||(e=[e]),ji(e)}(function(e){var t=!1;return Object(Hr.some)(Object(Xr.select)("core/block-editor").getBlocks(),(function(n){var r=n.attributes.ref===e;return r&&(t=n),r})),t}(r));o.length&&setTimeout((function(){Object(Xr.dispatch)("core").editEntityRecord("postType","wp_block",n.ref,{is_llms_field:o.length>0?"yes":"no"})}))}return e})),n(67);var Ei=Object(W.withInstanceId)((function(e){var t=e.options,n=e.fieldType,r=e.instanceId;return Object(z.createElement)(z.Fragment,null,t.map((function(e,t){return Object(z.createElement)("label",{htmlFor:"llms-".concat(n,"-").concat(r,"-").concat(t),key:t,style:{display:"block",pointerEvents:"none"}},Object(z.createElement)("input",{id:"llms-".concat(n,"-").concat(r,"-").concat(t),type:n,checked:"yes"===e.default,readOnly:!0})," ",e.text)})))}));var ki,Ci=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){return X()(this,o),r.apply(this,arguments)}return J()(o,[{key:"getFieldType",value:function(){var e=this.props.attributes.field;return-1!==["email","text","number","url","tel"].indexOf(e)?"input":e}},{key:"render",value:function(){var e=this.props,t=e.attributes,n=e.setAttributes,r=e.block,o=e.clientId,i=t.description,l=t.label,s=t.options,a=t.placeholder,c=t.required,u=r.supports.llms_edit_fill,f=[];c&&f.push("llms-is-required");var d=this.getFieldType();return Object(z.createElement)(z.Fragment,null,Object(z.createElement)("div",{className:"llms-field"},"html"!==d&&Object(z.createElement)(G.RichText,{tagName:"label",className:f.join(" "),value:l,onChange:function(e){n({label:e})},allowedFormats:["bold","italic"],"aria-label":l?Object($.__)("Field label"):Object($.__)("Empty field label; start writing to add a label"),placeholder:Object($.__)("Enter a label")}),"input"===d&&Object(z.createElement)("input",{onChange:function(e){return n({placeholder:e.target.value})},value:a,placeholder:Object($.__)("Add optional placeholder text","lifterlms")}),"password"===d&&Object(z.createElement)("input",{disabled:"disabed",type:"password",value:"F4K3p4$50Rd"}),"textarea"===d&&Object(z.createElement)("textarea",{rows:this.props.attributes.html_attrs.rows,onChange:function(e){return n({placeholder:e.target.value})},value:a,placeholder:Object($.__)("Add optional placeholder text","lifterlms")}),"select"===d&&Object(z.createElement)("select",null,Object(z.createElement)("option",null,function(){if(a)return a;if(!s.length)return"";var e=s[0].text,t=s.filter((function(e){return"yes"===e.default}));return t.length&&(e=t[0].text),e}())),Object(z.createElement)(G.RichText,{tagName:"span",value:i,onChange:function(e){n({description:e})},allowedFormats:["bold","strikethrough","link"],"aria-label":l?Object($.__)("Optional field description"):Object($.__)("Empty field description; start writing to add a description"),placeholder:Object($.__)("Add optional description text"),style:{color:"#808285",fontStyle:"italic"}}),("radio"===d||"checkbox"===d)&&Object(z.createElement)(Ei,{options:s,fieldType:d})),u.after&&Object(z.createElement)(K.Slot,{name:"llmsEditFill.after.".concat(u.after,".").concat(o)}))}}]),o}(z.Component),Si=n(36),Pi=n.n(Si),Ii=n(19),Di=n.n(Ii),Ri=new Uint8Array(16);function Ti(){if(!ki&&!(ki="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return ki(Ri)}for(var Mi=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i,Li=function(e){return"string"==typeof e&&Mi.test(e)},Ai=[],Ni=0;Ni<256;++Ni)Ai.push((Ni+256).toString(16).substr(1));var Fi=function(e,t,n){var r=(e=e||{}).random||(e.rng||Ti)();if(r[6]=15&r[6]|64,r[8]=63&r[8]|128,t){n=n||0;for(var o=0;o<16;++o)t[n+o]=r[o];return t}return function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(Ai[e[t+0]]+Ai[e[t+1]]+Ai[e[t+2]]+Ai[e[t+3]]+"-"+Ai[e[t+4]]+Ai[e[t+5]]+"-"+Ai[e[t+6]]+Ai[e[t+7]]+"-"+Ai[e[t+8]]+Ai[e[t+9]]+"-"+Ai[e[t+10]]+Ai[e[t+11]]+Ai[e[t+12]]+Ai[e[t+13]]+Ai[e[t+14]]+Ai[e[t+15]]).toLowerCase();if(!Li(n))throw TypeError("Stringified UUID is invalid");return n}(r)},Bi=n(37),Vi=n.n(Bi);var Ui=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o);for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return e=r.call.apply(r,[this].concat(n)),U()(ce()(e),"getDefaultClassName",(function(){return"llms-search--user"})),U()(ce()(e),"getSearchPath",(function(){return e.props.searchPath||"/wp/v2/users"})),U()(ce()(e),"formatSearchResultLabel",(function(e){return Object($.sprintf)(// Translators: %1$s = User's name; %2$s = User's id. +Object($._x)("%1$s (ID# %2$d)","User search result label","lifterlms"),e.name,e.id)})),e}return J()(o,[{key:"getSearchArgs",value:function(e){var t=Vi()(oe()(o.prototype),"getSearchArgs",this).call(this,e),n=this.props.roles;return n&&(t.roles=Array.isArray(n)?n.join(","):n),t}}]),o}($r);const Hi="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement,qi=Hi?me.useLayoutEffect:me.useEffect;function zi(e,t){const n=Object(me.useRef)();return Object(me.useMemo)(()=>{const t=e(n.current);return n.current=t,t},[...t])}function $i(){const e=Object(me.useRef)(null),t=Object(me.useCallback)(t=>{e.current=t},[]);return[e,t]}let Wi={};function Gi(e,t){return Object(me.useMemo)(()=>{if(t)return t;const n=null==Wi[e]?0:Wi[e]+1;return Wi[e]=n,`${e}-${n}`},[e,t])}function Ki(e){return(t,...n)=>n.reduce((t,n)=>{const r=Object.entries(n);for(const[n,o]of r){const r=t[n];null!=r&&(t[n]=r+e*o)}return t},{...t})}const Yi=Ki(1),Xi=Ki(-1),Qi=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return`translate3d(${t?Math.round(t):0}px, ${n?Math.round(n):0}px, 0)`}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return`scaleX(${t}) scaleY(${n})`}},Transform:{toString(e){if(e)return[Qi.Translate.toString(e),Qi.Scale.toString(e)].join(" ")}},Transition:{toString:({property:e,duration:t,easing:n})=>`${e} ${t}ms ${n}`}}),Ji={display:"none"};function Zi({id:e,value:t}){return be.a.createElement("div",{id:e,style:Ji},t)}const el={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};function tl({id:e,announcement:t}){return be.a.createElement("div",{id:e,style:el,role:"status","aria-live":"assertive","aria-atomic":!0},t)}const nl={draggable:"\n To pick up a draggable item, press the space bar.\n While dragging, use the arrow keys to move the item.\n Press space again to drop the item in its new position, or press escape to cancel.\n "},rl={onDragStart:e=>`Picked up draggable item ${e}.`,onDragOver:(e,t)=>t?`Draggable item ${e} was moved over droppable area ${t}.`:`Draggable item ${e} is no longer over a droppable area.`,onDragEnd:(e,t)=>t?`Draggable item ${e} was dropped over droppable area ${t}`:`Draggable item ${e} was dropped.`,onDragCancel:e=>`Dragging was cancelled. Draggable item ${e} was dropped.`};var ol;!function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"}(ol||(ol={}));const il=e=>ll(e,(e,t)=>e<t);function ll(e,t){if(0===e.length)return-1;let n=e[0],r=0;for(var o=1;o<e.length;o++)t(e[o],n)&&(r=o,n=e[o]);return r}function sl(...e){}function al(e,t){const{[e]:n,...r}=t;return r}const cl=Object(me.createContext)({activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,activeNodeClientRect:null,activators:[],ariaDescribedById:{draggable:""},containerNodeRect:null,dispatch:sl,draggableNodes:{},droppableRects:new Map,droppableContainers:{},over:null,overlayNode:{nodeRef:{current:null},rect:null,setRef:sl},scrollableAncestors:[],scrollableAncestorRects:[],recomputeLayouts:sl,windowRect:null,willRecomputeLayouts:!1}),ul=Object.freeze({x:0,y:0});function fl(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function dl(e){if(function(e){var t;return(null==(t=window)?void 0:t.TouchEvent)&&e instanceof TouchEvent}(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return function(e){var t;return(null==(t=window)?void 0:t.MouseEvent)&&e instanceof MouseEvent||e.type.includes("mouse")}(e)?{x:e.clientX,y:e.clientY}:{x:0,y:0}}function pl(e,t=e.offsetLeft,n=e.offsetTop){return{x:t+.5*e.width,y:n+.5*e.height}}const ml=(e,t)=>{const n=pl(t,t.left,t.top),r=e.map(([e,t])=>fl(pl(t),n)),o=il(r);return e[o]?e[o][0]:null};function bl(e){return function(t,...n){return n.reduce((t,n)=>({...t,top:t.top+e*n.y,bottom:t.bottom+e*n.y,left:t.left+e*n.x,right:t.right+e*n.x,offsetLeft:t.offsetLeft+e*n.x,offsetTop:t.offsetTop+e*n.y}),{...t})}}const hl=bl(1);function vl(e){const t=[];return e?function e(n){return n?n instanceof Document&&null!=n.scrollingElement?(t.push(n.scrollingElement),t):!(n instanceof HTMLElement)||n instanceof SVGElement?t:(function(e){const t=window.getComputedStyle(e),n=/(auto|scroll|overlay)/;return null!=["overflow","overflowX","overflowY"].find(e=>{const r=t[e];return"string"==typeof r&&n.test(r)})}(n)&&t.push(n),e(n.parentNode)):t}(e.parentNode):t}function gl(e){return Hi?e===document.scrollingElement||e instanceof Document?window:e instanceof HTMLElement?e:null:null}function yl(e){return e instanceof Window?{x:e.scrollX,y:e.scrollY}:{x:e.scrollLeft,y:e.scrollTop}}var Ol;function _l(e){const t={x:0,y:0},n={x:e.scrollWidth-e.clientWidth,y:e.scrollHeight-e.clientHeight};return{isTop:e.scrollTop<=t.y,isLeft:e.scrollLeft<=t.x,isBottom:e.scrollTop>=n.y,isRight:e.scrollLeft>=n.x,maxScroll:n,minScroll:t}}!function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"}(Ol||(Ol={}));const jl={x:.2,y:.2};function wl(e,t,{top:n,left:r,right:o,bottom:i},l=10,s=jl){const{clientHeight:a,clientWidth:c}=e,u=(f=e,Hi&&f&&f===document.scrollingElement?{top:0,left:0,right:c,bottom:a,width:c,height:a}:t);var f;const{isTop:d,isBottom:p,isLeft:m,isRight:b}=_l(e),h={x:0,y:0},v={x:0,y:0},g=u.height*s.y,y=u.width*s.x;return!d&&n<=u.top+g?(h.y=Ol.Backward,v.y=l*Math.abs((u.top+g-n)/g)):!p&&i>=u.bottom-g&&(h.y=Ol.Forward,v.y=l*Math.abs((u.bottom-g-i)/g)),!b&&o>=u.right-y?(h.x=Ol.Forward,v.x=l*Math.abs((u.right-y-o)/y)):!m&&r<=u.left+y&&(h.x=Ol.Backward,v.x=l*Math.abs((u.left+y-r)/y)),{direction:h,speed:v}}function xl(e){if(e===document.scrollingElement){const{innerWidth:e,innerHeight:t}=window;return{top:0,left:0,right:e,bottom:t,width:e,height:t}}const{top:t,left:n,right:r,bottom:o}=e.getBoundingClientRect();return{top:t,left:n,right:r,bottom:o,width:e.clientWidth,height:e.clientHeight}}function El(e){return e.reduce((e,t)=>Yi(e,yl(t)),ul)}function kl(e){const{offsetWidth:t,offsetHeight:n}=e,{x:r,y:o}=function e(t,n,r=ul){if(!(t&&t instanceof HTMLElement))return r;const o={x:r.x+t.offsetLeft,y:r.y+t.offsetTop};return t.offsetParent===n?o:e(t.offsetParent,n,o)}(e,null);return{width:t,height:n,offsetTop:o,offsetLeft:r}}function Cl(e){if(e instanceof Window){const e=window.innerWidth,t=window.innerHeight;return{top:0,left:0,right:e,bottom:t,width:e,height:t,offsetTop:0,offsetLeft:0}}const{offsetTop:t,offsetLeft:n}=kl(e),{width:r,height:o,top:i,bottom:l,left:s,right:a}=e.getBoundingClientRect();return{width:r,height:o,top:i,bottom:l,right:a,left:s,offsetTop:t,offsetLeft:n}}function Sl(e){const{width:t,height:n,offsetTop:r,offsetLeft:o}=kl(e),i=El(vl(e)),l=r-i.y,s=o-i.x;return{width:t,height:n,top:l,bottom:l+n,right:s+t,left:s,offsetTop:r,offsetLeft:o}}function Pl(e){return"top"in e}function Il(e,t=e.offsetLeft,n=e.offsetTop){return[{x:t,y:n},{x:t+e.width,y:n},{x:t,y:n+e.height},{x:t+e.width,y:n+e.height}]}const Dl=(e,t)=>{const n=e.map(([e,n])=>function(e,t){const n=Math.max(t.top,e.offsetTop),r=Math.max(t.left,e.offsetLeft),o=Math.min(t.left+t.width,e.offsetLeft+e.width),i=Math.min(t.top+t.height,e.offsetTop+e.height),l=o-r,s=i-n;if(r<o&&n<i){const n=t.width*t.height,r=e.width*e.height,o=l*s;return Number((o/(n+r-o)).toFixed(4))}return 0}(n,t)),r=ll(n,(e,t)=>e>t);return n[r]<=0?null:e[r]?e[r][0]:null};function Rl(e){return e instanceof HTMLElement?e.ownerDocument:document}function Tl(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:{},translate:{x:0,y:0}},droppable:{containers:{}}}}function Ml(e,t){switch(t.type){case ol.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case ol.DragMove:return e.draggable.active?{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}}:e;case ol.DragEnd:case ol.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case ol.RegisterDroppable:{const{element:n}=t,{id:r}=n;return{...e,droppable:{...e.droppable,containers:{...e.droppable.containers,[r]:n}}}}case ol.SetDroppableDisabled:{const{id:n,disabled:r}=t,o=e.droppable.containers[n];return o?{...e,droppable:{...e.droppable,containers:{...e.droppable.containers,[n]:{...o,disabled:r}}}}:e}case ol.UnregisterDroppable:{const{id:n}=t;return{...e,droppable:{...e.droppable,containers:al(n,e.droppable.containers)}}}default:return e}}const Ll=Object(me.createContext)({type:null,event:null});function Al({announcements:e=rl,hiddenTextDescribedById:t,screenReaderInstructions:n}){const{announce:r,announcement:o}=function(){const[e,t]=Object(me.useState)("");return{announce:Object(me.useCallback)(e=>{null!=e&&t(e)},[]),announcement:e}}(),i=Gi("DndLiveRegion"),[l,s]=Object(me.useState)(!1);return Object(me.useEffect)(()=>{s(!0)},[]),function({onDragStart:e,onDragMove:t,onDragOver:n,onDragEnd:r,onDragCancel:o}){const i=Object(me.useContext)(Ll),l=Object(me.useRef)(i);Object(me.useEffect)(()=>{if(i!==l.current){const{type:s,event:a}=i;switch(s){case ol.DragStart:null==e||e(a);break;case ol.DragMove:null==t||t(a);break;case ol.DragOver:null==n||n(a);break;case ol.DragCancel:null==o||o(a);break;case ol.DragEnd:null==r||r(a)}l.current=i}},[i,e,t,n,r,o])}(Object(me.useMemo)(()=>({onDragStart({active:t}){r(e.onDragStart(t.id))},onDragMove({active:t,over:n}){e.onDragMove&&r(e.onDragMove(t.id,null==n?void 0:n.id))},onDragOver({active:t,over:n}){r(e.onDragOver(t.id,null==n?void 0:n.id))},onDragEnd({active:t,over:n}){r(e.onDragEnd(t.id,null==n?void 0:n.id))},onDragCancel({active:t}){r(e.onDragCancel(t.id))}}),[r,e])),l?Object(Gt.createPortal)(be.a.createElement(be.a.Fragment,null,be.a.createElement(Zi,{id:t,value:n.draggable}),be.a.createElement(tl,{id:i,announcement:o})),document.body):null}var Nl,Fl,Bl,Vl;function Ul(e){const t=Object(me.useRef)(e);return qi(()=>{t.current!==e&&(t.current=e)},[e]),t}!function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"}(Nl||(Nl={})),function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"}(Fl||(Fl={})),function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"}(Bl||(Bl={})),function(e){e.Optimized="optimized"}(Vl||(Vl={}));const Hl=new Map;const ql={strategy:Bl.WhileDragging,frequency:Vl.Optimized},zl=[],$l=Kl(Cl),Wl=Yl(Cl),Gl=Kl(Sl);function Kl(e){return function(t,n){const r=Object(me.useRef)(t);return zi(o=>t?n||!o&&t||t!==r.current?t instanceof HTMLElement&&null==t.parentNode?null:e(t):null!=o?o:null:null,[t,n])}}function Yl(e){const t=[];return function(n,r){const o=Object(me.useRef)(n);return zi(i=>n.length?r||!i&&n.length||n!==o.current?n.map(t=>e(t)):null!=i?i:t:t,[n,r])}}function Xl(e,t){return Object(me.useMemo)(()=>({sensor:e,options:null!=t?t:{}}),[e,t])}class Ql{constructor(e){this.target=e,this.listeners=[]}add(e,t,n){this.target.addEventListener(e,t,n),this.listeners.push({eventName:e,handler:t})}removeAll(){this.listeners.forEach(({eventName:e,handler:t})=>this.target.removeEventListener(e,t))}}function Jl(e,t){const n=Math.abs(e.x),r=Math.abs(e.y);return"number"==typeof t?Math.sqrt(n**2+r**2)>t:"x"in t&&"y"in t?n>t.x&&r>t.y:"x"in t?n>t.x:"y"in t&&r>t.y}var Zl;!function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter"}(Zl||(Zl={}));const es={start:[Zl.Space,Zl.Enter],cancel:[Zl.Esc],end:[Zl.Space,Zl.Enter]},ts=(e,{currentCoordinates:t})=>{switch(e.code){case Zl.Right:return{...t,x:t.x+25};case Zl.Left:return{...t,x:t.x-25};case Zl.Down:return{...t,y:t.y+25};case Zl.Up:return{...t,y:t.y-25}}};class ns{constructor(e){this.props=e,this.autoScrollEnabled=!1,this.coordinates=ul;const{event:{target:t}}=e;this.props=e,this.listeners=new Ql(Rl(t)),this.windowListeners=new Ql(function(e){var t;return null!=(t=Rl(e).defaultView)?t:window}(t)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),setTimeout(()=>{this.listeners.add("keydown",this.handleKeyDown),this.windowListeners.add("resize",this.handleCancel)})}handleStart(){const{activeNode:e,onStart:t}=this.props;if(!e.node.current)throw new Error("Active draggable node is undefined");const n=Cl(e.node.current),r={x:n.left,y:n.top};this.coordinates=r,t(r)}handleKeyDown(e){if(e instanceof KeyboardEvent){const{coordinates:t}=this,{active:n,context:r,options:o}=this.props,{keyboardCodes:i=es,coordinateGetter:l=ts,scrollBehavior:s="smooth"}=o,{code:a}=e;if(i.end.includes(a))return void this.handleEnd(e);if(i.cancel.includes(a))return void this.handleCancel(e);const c=l(e,{active:n,context:r.current,currentCoordinates:t});if(c){const n={x:0,y:0},{scrollableAncestors:o}=r.current;for(const r of o){const o=e.code,i=Xi(c,t),{isTop:l,isRight:a,isLeft:u,isBottom:f,maxScroll:d,minScroll:p}=_l(r),m=xl(r),b={x:Math.min(o===Zl.Right?m.right-m.width/2:m.right,Math.max(o===Zl.Right?m.left:m.left+m.width/2,c.x)),y:Math.min(o===Zl.Down?m.bottom-m.height/2:m.bottom,Math.max(o===Zl.Down?m.top:m.top+m.height/2,c.y))},h=o===Zl.Right&&!a||o===Zl.Left&&!u,v=o===Zl.Down&&!f||o===Zl.Up&&!l;if(h&&b.x!==c.x){if(o===Zl.Right&&r.scrollLeft+i.x<=d.x||o===Zl.Left&&r.scrollLeft+i.x>=p.x)return void r.scrollBy({left:i.x,behavior:s});n.x=o===Zl.Right?r.scrollLeft-d.x:r.scrollLeft-p.x,r.scrollBy({left:-n.x,behavior:s});break}if(v&&b.y!==c.y){if(o===Zl.Down&&r.scrollTop+i.y<=d.y||o===Zl.Up&&r.scrollTop+i.y>=p.y)return void r.scrollBy({top:i.y,behavior:s});n.y=o===Zl.Down?r.scrollTop-d.y:r.scrollTop-p.y,r.scrollBy({top:-n.y,behavior:s});break}}this.handleMove(e,Yi(c,n))}}}handleMove(e,t){const{onMove:n}=this.props;e.preventDefault(),n(t),this.coordinates=t}handleEnd(e){const{onEnd:t}=this.props;e.preventDefault(),this.detach(),t()}handleCancel(e){const{onCancel:t}=this.props;e.preventDefault(),this.detach(),t()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}function rs(e){return Boolean(e&&"distance"in e)}function os(e){return Boolean(e&&"delay"in e)}var is;ns.activators=[{eventName:"onKeyDown",handler:(e,{keyboardCodes:t=es,onActivation:n})=>{const{code:r}=e.nativeEvent;return!!t.start.includes(r)&&(e.preventDefault(),null==n||n({event:e.nativeEvent}),!0)}}],function(e){e.Keydown="keydown"}(is||(is={}));class ls{constructor(e,t,n=function(e){return e instanceof EventTarget?e:Rl(e)}(e.event.target)){this.props=e,this.events=t,this.autoScrollEnabled=!0,this.activated=!1,this.timeoutId=null;const{event:r}=e;this.props=e,this.events=t,this.ownerDocument=Rl(r.target),this.listeners=new Ql(n),this.initialCoordinates=dl(r),this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.attach()}attach(){const{events:e,props:{options:{activationConstraint:t}}}=this;if(this.listeners.add(e.move.name,this.handleMove,!1),this.listeners.add(e.end.name,this.handleEnd),this.ownerDocument.addEventListener(is.Keydown,this.handleKeydown),t){if(rs(t))return;if(os(t))return void(this.timeoutId=setTimeout(this.handleStart,t.delay))}this.handleStart()}detach(){this.listeners.removeAll(),this.ownerDocument.removeEventListener(is.Keydown,this.handleKeydown),null!==this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handleStart(){const{initialCoordinates:e}=this,{onStart:t}=this.props;e&&(this.activated=!0,t(e))}handleMove(e){const{activated:t,initialCoordinates:n,props:r}=this,{onMove:o,options:{activationConstraint:i}}=r;if(!n)return;const l=dl(e),s=Xi(n,l);if(!t&&i){if(os(i))return Jl(s,i.tolerance)?this.handleCancel():void 0;if(rs(i))return Jl(s,i.distance)?this.handleStart():void 0}e.cancelable&&e.preventDefault(),o(l)}handleEnd(){const{onEnd:e}=this.props;this.detach(),e()}handleCancel(){const{onCancel:e}=this.props;this.detach(),e()}handleKeydown(e){e.code===Zl.Esc&&this.handleCancel()}}const ss={move:{name:"pointermove"},end:{name:"pointerup"}};class as extends ls{constructor(e){const{event:t}=e,n=Rl(t.target);super(e,ss,n)}}as.activators=[{eventName:"onPointerDown",handler:({nativeEvent:e},{onActivation:t})=>!(!e.isPrimary||0!==e.button||(null==t||t({event:e}),0))}];const cs={move:{name:"mousemove"},end:{name:"mouseup"}};var us;!function(e){e[e.RightClick=2]="RightClick"}(us||(us={})),class extends ls{constructor(e){super(e,cs,Rl(e.event.target))}}.activators=[{eventName:"onMouseDown",handler:({nativeEvent:e},{onActivation:t})=>e.button!==us.RightClick&&(null==t||t({event:e}),!0)}];const fs={move:{name:"touchmove"},end:{name:"touchend"}};(class extends ls{constructor(e){super(e,fs)}}).activators=[{eventName:"onTouchStart",handler:({nativeEvent:e},{onActivation:t})=>{const{touches:n}=e;return!(n.length>1||(null==t||t({event:e}),0))}}];const ds=[{sensor:as,options:{}},{sensor:ns,options:{}}],ps={current:{}},ms=Object(me.createContext)({...ul,scaleX:1,scaleY:1}),bs=Object(me.memo)((function({id:e,autoScroll:t=!0,announcements:n,children:r,sensors:o=ds,collisionDetection:i=Dl,layoutMeasuring:l,modifiers:s,screenReaderInstructions:a=nl,...c}){var u,f,d;const p=Object(me.useReducer)(Ml,void 0,Tl),[m,b]=p,[h,v]=Object(me.useState)(()=>({type:null,event:null})),{draggable:{active:g,nodes:y,translate:O},droppable:{containers:_}}=m,j=g?y[g]:null,w=Object(me.useRef)({initial:null,translated:null}),x=Object(me.useMemo)(()=>{var e;return null!=g?{id:g,data:null!=(e=null==j?void 0:j.data)?e:ps,rect:w}:null},[g,j]),E=Object(me.useRef)(null),[k,C]=Object(me.useState)(null),[S,P]=Object(me.useState)(null),I=Object(me.useRef)(c),D=Gi("DndDescribedBy",e),{layoutRectMap:R,recomputeLayouts:T,willRecomputeLayouts:M}=function(e,{dragging:t,dependencies:n,config:r}){const[o,i]=Object(me.useState)(!1),{frequency:l,strategy:s}=(a=r)?{...ql,...a}:ql;var a;const c=Object(me.useRef)(e),u=Object(me.useCallback)(()=>i(!0),[]),f=Object(me.useRef)(null),d=function(){switch(s){case Bl.Always:return!1;case Bl.BeforeDragging:return t;default:return!t}}(),p=zi(n=>{if(d&&!t)return Hl;if(!n||n===Hl||c.current!==e||o){for(let t of Object.values(e))t&&(t.rect.current=t.node.current?kl(t.node.current):null);return function(e){const t=new Map;if(e)for(const n of Object.values(e)){if(!n)continue;const{id:e,rect:r,disabled:o}=n;o||null==r.current||t.set(e,r.current)}return t}(e)}return n},[e,t,d,o]);return Object(me.useEffect)(()=>{c.current=e},[e]),Object(me.useEffect)(()=>{o&&i(!1)},[o]),Object(me.useEffect)((function(){d||requestAnimationFrame(u)}),[t,d]),Object(me.useEffect)((function(){d||"number"!=typeof l||null!==f.current||(f.current=setTimeout(()=>{u(),f.current=null},l))}),[l,d,u,...n]),{layoutRectMap:p,recomputeLayouts:u,willRecomputeLayouts:o}}(_,{dragging:null!=g,dependencies:[O.x,O.y],config:l}),L=function(e,t){const n=null!==t?e[t]:void 0,r=n?n.node.current:null;return zi(e=>{var n;return null===t?null:null!=(n=null!=r?r:e)?n:null},[r,t])}(y,g),A=S?dl(S):null,N=Gl(L),F=$l(L),B=Object(me.useRef)(null),V=(H=B.current,(U=N)&&H?{x:U.left-H.left,y:U.top-H.top}:ul);var U,H;const q=Object(me.useRef)({active:null,activeNode:L,collisionRect:null,droppableRects:R,draggableNodes:y,draggingNodeRect:null,droppableContainers:_,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null,translatedRect:null}),z=function(e,t){var n,r;return e&&null!=(n=null==(r=t[e])?void 0:r.node.current)?n:null}(null!=(u=null==(f=q.current.over)?void 0:f.id)?u:null,_),$=$l(L?L.ownerDocument.defaultView:null),W=$l(L?L.parentElement:null),G=function(e){const t=Object(me.useRef)(e),n=zi(n=>e?n&&e&&t.current&&e.parentNode===t.current.parentNode?n:vl(e):zl,[e]);return Object(me.useEffect)(()=>{t.current=e},[e]),n}(g?null!=z?z:L:null),K=Wl(G),[Y,X]=$i(),Q=$l(g?Y.current:null,M),J=null!=Q?Q:F,Z=function(e,{transform:t,...n}){return(null==e?void 0:e.length)?e.reduce((e,t)=>t({transform:e,...n}),t):t}(s,{transform:{x:O.x-V.x,y:O.y-V.y,scaleX:1,scaleY:1},active:x,over:q.current.over,activeNodeRect:F,draggingNodeRect:J,containerNodeRect:W,overlayNodeRect:Q,scrollableAncestors:G,scrollableAncestorRects:K,windowRect:$}),ee=A?Yi(A,O):null,te=function(e){const[t,n]=Object(me.useState)(null),r=Object(me.useRef)(e),o=Object(me.useCallback)(e=>{const t=gl(e.target);t&&n(e=>e?(e.set(t,yl(t)),new Map(e)):null)},[]);return Object(me.useEffect)(()=>{const t=r.current;if(e!==t){i(t);const l=e.map(e=>{const t=gl(e);return t?(t.addEventListener("scroll",o,{passive:!0}),[t,yl(t)]):null}).filter(e=>null!=e);n(l.length?new Map(l):null),r.current=e}return()=>{i(e),i(t)};function i(e){e.forEach(e=>{const t=gl(e);null==t||t.removeEventListener("scroll",o)})}},[o,e]),Object(me.useMemo)(()=>e.length?t?Array.from(t.values()).reduce((e,t)=>Yi(e,t),ul):El(e):ul,[e,t])}(G),ne=Yi(Z,te),re=N?hl(N,Z):null,oe=re?hl(re,te):null,ie=function(e,t){var n;return e&&null!=(n=t[e])?n:null}(x&&oe?i(Array.from(R.entries()),oe):null,_),le=Object(me.useMemo)(()=>ie&&ie.rect.current?{id:ie.id,rect:ie.rect.current,data:ie.data,disabled:ie.disabled}:null,[ie]),se=function(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}(Z,null!=(d=null==ie?void 0:ie.rect.current)?d:null,N),ae=Object(me.useCallback)((e,{sensor:t,options:n})=>{if(!E.current)return;const r=y[E.current];if(!r)return;const o=new t({active:E.current,activeNode:r,event:e.nativeEvent,options:n,context:q,onStart(e){const t=E.current;if(!t)return;const n=y[t];if(!n)return;const{onDragStart:r}=I.current,o={active:{id:t,data:n.data,rect:w}};b({type:ol.DragStart,initialCoordinates:e,active:t}),v({type:ol.DragStart,event:o}),null==r||r(o)},onMove(e){b({type:ol.DragMove,coordinates:e})},onEnd:i(ol.DragEnd),onCancel:i(ol.DragCancel)});function i(e){return async function(){const{active:t,over:n,scrollAdjustedTranslate:r}=q.current;let o=null;if(t&&r){const{cancelDrop:i}=I.current;o={active:t,delta:r,over:n},e===ol.DragEnd&&"function"==typeof i&&await Promise.resolve(i(o))&&(e=ol.DragCancel)}if(E.current=null,b({type:e}),C(null),P(null),o){const{onDragCancel:t,onDragEnd:n}=I.current,r=e===ol.DragEnd?n:t;v({type:e,event:o}),null==r||r(o)}}}C(o),P(e.nativeEvent)},[b,y]),ce=function(e,t){return Object(me.useMemo)(()=>e.reduce((e,n)=>{const{sensor:r}=n;return[...e,...r.activators.map(e=>({eventName:e.eventName,handler:t(e.handler,n)}))]},[]),[e,t])}(o,Object(me.useCallback)((e,t)=>(n,r)=>{const o=n.nativeEvent;null!==E.current||o.dndKit||o.defaultPrevented||!0===e(n,t.options)&&(o.dndKit={capturedBy:t.sensor},E.current=r,ae(n,t))},[ae]));qi(()=>{I.current=c},Object.values(c)),Object(me.useEffect)(()=>{x||(B.current=null),x&&N&&!B.current&&(B.current=N)},[N,x]),Object(me.useEffect)(()=>{const{onDragMove:e}=I.current,{active:t,over:n}=q.current;if(!t)return;const r={active:t,delta:{x:ne.x,y:ne.y},over:n};v({type:ol.DragMove,event:r}),null==e||e(r)},[ne.x,ne.y]),Object(me.useEffect)(()=>{const{active:e,scrollAdjustedTranslate:t}=q.current;if(!e||!E.current||!t)return;const{onDragOver:n}=I.current,r={active:e,delta:{x:t.x,y:t.y},over:le};v({type:ol.DragOver,event:r}),null==n||n(r)},[null==le?void 0:le.id]),qi(()=>{q.current={active:x,activeNode:L,collisionRect:oe,droppableRects:R,draggableNodes:y,draggingNodeRect:J,droppableContainers:_,over:le,scrollableAncestors:G,scrollAdjustedTranslate:ne,translatedRect:re},w.current={initial:J,translated:re}},[x,L,oe,y,J,R,_,le,G,ne,re]),function({acceleration:e,activator:t=Nl.Pointer,canScroll:n,draggingRect:r,enabled:o,interval:i=5,order:l=Fl.TreeOrder,pointerCoordinates:s,scrollableAncestors:a,scrollableAncestorRects:c,threshold:u}){const[f,d]=function(){const e=Object(me.useRef)(null);return[Object(me.useCallback)((t,n)=>{e.current=setInterval(t,n)},[]),Object(me.useCallback)(()=>{null!==e.current&&(clearInterval(e.current),e.current=null)},[])]}(),p=Object(me.useRef)({x:1,y:1}),m=Object(me.useMemo)(()=>{switch(t){case Nl.Pointer:return s?{top:s.y,bottom:s.y,left:s.x,right:s.x}:null;case Nl.DraggableRect:return r}return null},[t,r,s]),b=Object(me.useRef)(ul),h=Object(me.useRef)(null),v=Object(me.useCallback)(()=>{const e=h.current;if(!e)return;const t=p.current.x*b.current.x,n=p.current.y*b.current.y;e.scrollBy(t,n)},[]),g=Object(me.useMemo)(()=>l===Fl.TreeOrder?[...a].reverse():a,[l,a]);Object(me.useEffect)(()=>{if(o&&a.length&&m){for(const t of g){if(!1===(null==n?void 0:n(t)))continue;const r=a.indexOf(t),o=c[r];if(!o)continue;const{direction:l,speed:s}=wl(t,o,m,e,u);if(s.x>0||s.y>0)return d(),h.current=t,f(v,i),p.current=s,void(b.current=l)}p.current={x:0,y:0},b.current={x:0,y:0},d()}else d()},[e,v,n,d,o,i,JSON.stringify(m),f,a,g,c,JSON.stringify(u)])}({...function(){const e=!1===(null==k?void 0:k.autoScrollEnabled),n="object"==typeof t?!1===t.enabled:!1===t,r=!e&&!n;return"object"==typeof t?{...t,enabled:r}:{enabled:r}}(),draggingRect:re,pointerCoordinates:ee,scrollableAncestors:G,scrollableAncestorRects:K});const ue=Object(me.useMemo)(()=>({active:x,activeNode:L,activeNodeRect:N,activeNodeClientRect:F,activatorEvent:S,activators:ce,ariaDescribedById:{draggable:D},overlayNode:{nodeRef:Y,rect:Q,setRef:X},containerNodeRect:W,dispatch:b,draggableNodes:y,droppableContainers:_,droppableRects:R,over:le,recomputeLayouts:T,scrollableAncestors:G,scrollableAncestorRects:K,willRecomputeLayouts:M,windowRect:$}),[x,L,F,N,S,ce,W,Q,Y,b,y,D,_,R,le,T,G,K,X,M,$]);return be.a.createElement(Ll.Provider,{value:h},be.a.createElement(cl.Provider,{value:ue},be.a.createElement(ms.Provider,{value:se},r)),be.a.createElement(Al,{announcements:n,hiddenTextDescribedById:D,screenReaderInstructions:a}))})),hs=Object(me.createContext)(null),vs="button";function gs(e,t,n){const r=e.slice();return r.splice(n<0?r.length+n:n,0,r.splice(t,1)[0]),r}function ys(e){return null!==e&&e>=0}const Os=({layoutRects:e,activeIndex:t,overIndex:n,index:r})=>{const o=gs(e,n,t),i=e[r],l=o[r];return l&&i?{x:l.offsetLeft-i.offsetLeft,y:l.offsetTop-i.offsetTop,scaleX:l.width/i.width,scaleY:l.height/i.height}:null},_s={scaleX:1,scaleY:1},js=({activeIndex:e,activeNodeRect:t,index:n,layoutRects:r,overIndex:o})=>{var i;const l=null!=(i=r[e])?i:t;if(!l)return null;if(n===e){const t=r[o];return t?{x:0,y:e<o?t.offsetTop+t.height-(l.offsetTop+l.height):t.offsetTop-l.offsetTop,..._s}:null}const s=function(e,t,n){const r=e[t],o=e[t-1],i=e[t+1];return r?n<t?o?r.offsetTop-(o.offsetTop+o.height):i?i.offsetTop-(r.offsetTop+r.height):0:i?i.offsetTop-(r.offsetTop+r.height):o?r.offsetTop-(o.offsetTop+o.height):0:0}(r,n,e);return n>e&&n<=o?{x:0,y:-l.height-s,..._s}:n<e&&n>=o?{x:0,y:l.height+s,..._s}:{x:0,y:0,..._s}},ws=be.a.createContext({activeIndex:-1,containerId:"Sortable",disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:Os,wasSorting:{current:!1}});function xs({children:e,id:t,items:n,strategy:r=Os}){const{active:o,overlayNode:i,droppableRects:l,over:s,recomputeLayouts:a,willRecomputeLayouts:c}=Object(me.useContext)(cl),u=Gi("Sortable",t),f=Boolean(null!==i.rect),d=Object(me.useMemo)(()=>n.map(e=>"string"==typeof e?e:e.id),[n]),p=o?d.indexOf(o.id):-1,m=-1!==p,b=Object(me.useRef)(m),h=s?d.indexOf(s.id):-1,v=Object(me.useRef)(d),g=function(e,t){return e.reduce((e,n,r)=>{const o=t.get(n);return o&&(e[r]=o),e},Array(e.length))}(d,l),y=(O=d,_=v.current,!(O.join()===_.join()));var O,_;const j=-1!==h&&-1===p||y;qi(()=>{y&&m&&!c&&a()},[y,m,a,c]),Object(me.useEffect)(()=>{v.current=d},[d]),Object(me.useEffect)(()=>{requestAnimationFrame(()=>{b.current=m})},[m]);const w=Object(me.useMemo)(()=>({activeIndex:p,containerId:u,disableTransforms:j,items:d,overIndex:h,useDragOverlay:f,sortedRects:g,strategy:r,wasSorting:b}),[p,u,j,d,h,g,f,r,b]);return be.a.createElement(ws.Provider,{value:w},e)}const Es=({isSorting:e,index:t,newIndex:n,transition:r})=>!(!r||!e&&n===t),ks={duration:200,easing:"ease"},Cs=Qi.Transition.toString({property:"transform",duration:0,easing:"linear"}),Ss={roleDescription:"sortable"};function Ps({animateLayoutChanges:e=Es,attributes:t,disabled:n,data:r,id:o,strategy:i,transition:l=ks}){const{items:s,containerId:a,activeIndex:c,disableTransforms:u,sortedRects:f,overIndex:d,useDragOverlay:p,strategy:m,wasSorting:b}=Object(me.useContext)(ws),h=s.indexOf(o),v=Object(me.useMemo)(()=>({sortable:{containerId:a,index:h,items:s},...r}),[a,r,h,s]),{rect:g,node:y,setNodeRef:O}=function({data:e,disabled:t=!1,id:n}){const{active:r,dispatch:o,over:i}=Object(me.useContext)(cl),l=Object(me.useRef)(null),[s,a]=$i(),c=Ul(e);return qi(()=>(o({type:ol.RegisterDroppable,element:{id:n,disabled:t,node:s,rect:l,data:c}}),()=>o({type:ol.UnregisterDroppable,id:n})),[n]),Object(me.useEffect)(()=>{o({type:ol.SetDroppableDisabled,id:n,disabled:t})},[t]),{active:r,rect:l,isOver:(null==i?void 0:i.id)===n,node:s,over:i,setNodeRef:a}}({id:o,data:v}),{active:_,activeNodeRect:j,activatorEvent:w,attributes:x,setNodeRef:E,listeners:k,isDragging:C,over:S,transform:P}=function({id:e,data:t,disabled:n=!1,attributes:r}){const{active:o,activeNodeRect:i,activatorEvent:l,ariaDescribedById:s,draggableNodes:a,droppableRects:c,activators:u,over:f}=Object(me.useContext)(cl),{role:d=vs,roleDescription:p="draggable",tabIndex:m=0}=null!=r?r:{},b=(null==o?void 0:o.id)===e,h=Object(me.useContext)(b?ms:hs),[v,g]=$i(),y=function(e,t){return Object(me.useMemo)(()=>e.reduce((e,{eventName:n,handler:r})=>(e[n]=e=>{r(e,t)},e),{}),[e,t])}(u,e),O=Ul(t);return Object(me.useEffect)(()=>(a[e]={node:v,data:O},()=>{delete a[e]}),[a,e]),{active:o,activeNodeRect:i,activatorEvent:l,attributes:Object(me.useMemo)(()=>({role:d,tabIndex:m,"aria-pressed":!(!b||d!==vs)||void 0,"aria-roledescription":p,"aria-describedby":s.draggable}),[d,m,b,p,s.draggable]),droppableRects:c,isDragging:b,listeners:n?void 0:y,node:v,over:f,setNodeRef:g,transform:h}}({id:o,data:v,attributes:{...Ss,...t},disabled:n}),I=function(...e){return Object(me.useMemo)(()=>t=>{e.forEach(e=>e(t))},e)}(O,E),D=Boolean(_),R=D&&b.current&&!u&&ys(c)&&ys(d),T=!p&&C,M=T&&R?P:null,L=R?null!=M?M:(null!=i?i:m)({layoutRects:f,activeNodeRect:j,activeIndex:c,overIndex:d,index:h}):null,A=ys(c)&&ys(d)?gs(s,c,d).indexOf(o):h,N=Object(me.useRef)(A),F=e({active:_,isDragging:C,isSorting:D,id:o,index:h,items:s,newIndex:N.current,transition:l,wasSorting:b.current}),B=function({rect:e,disabled:t,index:n,node:r}){const[o,i]=Object(me.useState)(null),l=Object(me.useRef)(n);return Object(me.useEffect)(()=>{if(!t&&n!==l.current&&r.current){const t=e.current;if(t){const e=Cl(r.current),n={x:t.offsetLeft-e.offsetLeft,y:t.offsetTop-e.offsetTop,scaleX:t.width/e.width,scaleY:t.height/e.height};(n.x||n.y)&&i(n)}}n!==l.current&&(l.current=n)},[t,n,r,e]),Object(me.useEffect)(()=>{o&&requestAnimationFrame(()=>{i(null)})},[o]),o}({disabled:!F,index:h,node:y,rect:g});return Object(me.useEffect)(()=>{D&&(N.current=A)},[D,A]),{active:_,attributes:x,activatorEvent:w,rect:g,index:h,isSorting:D,isDragging:C,listeners:k,node:y,overIndex:d,over:S,setNodeRef:I,setDroppableNodeRef:O,setDraggableNodeRef:E,transform:null!=B?B:L,transition:B?Cs:T||!l?null:D||F?Qi.Transition.toString({...l,property:"transform"}):null}}const Is=[Zl.Down,Zl.Right,Zl.Up,Zl.Left],Ds=(e,{context:{droppableContainers:t,translatedRect:n,scrollableAncestors:r}})=>{if(Is.includes(e.code)){if(e.preventDefault(),!n)return;const i=[];Object.entries(t).forEach(([t,r])=>{if(null==r?void 0:r.disabled)return;const o=null==r?void 0:r.node.current;if(!o)return;const l=Sl(o);switch(e.code){case Zl.Down:n.top+n.height<=l.top&&i.push([t,l]);break;case Zl.Up:n.top>=l.top+l.height&&i.push([t,l]);break;case Zl.Left:n.left>=l.left+l.width&&i.push([t,l]);break;case Zl.Right:n.left+n.width<=l.left&&i.push([t,l])}});const l=((e,t)=>{const n=Il(t,t.left,t.top),r=e.map(([e,t])=>{const r=Il(t,Pl(t)?t.left:void 0,Pl(t)?t.top:void 0),o=n.reduce((e,t,n)=>e+fl(r[n],t),0);return Number((o/4).toFixed(4))}),o=il(r);return e[o]?e[o][0]:null})(i,n);if(l){var o;const e=null==(o=t[l])?void 0:o.node.current;if(e){const t=vl(e).some((e,t)=>r[t]!==e),o=Sl(e),i=t?{x:0,y:0}:{x:n.width-o.width,y:n.height-o.height};return{x:o.left-i.x,y:o.top-i.y}}}}};const Rs=({transform:e})=>({...e,x:0}),Ts=({transform:e,activeNodeRect:t,windowRect:n})=>t&&n?function(e,t,n){const r={...e};return t.top+e.y<=n.top?r.y=n.top-t.top:t.bottom+e.y>=n.top+n.height&&(r.y=n.top+n.height-t.bottom),t.left+e.x<=n.left?r.x=n.left-t.left:t.right+e.x>=n.left+n.width&&(r.x=n.left+n.width-t.right),r}(e,t,n):e;var Ms=Object(z.createElement)("svg",{width:"18",height:"18",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 18 18"},Object(z.createElement)("path",{d:"M5 4h2V2H5v2zm6-2v2h2V2h-2zm-6 8h2V8H5v2zm6 0h2V8h-2v2zm-6 6h2v-2H5v2zm6 0h2v-2h-2v2z"})),Ls=function(e){var t=e.label,n=e.setNodeRef,r=e.listeners;return t=t||Object($.__)("Reorder instructor","lifterlms"),Object(z.createElement)(K.Button,Tt()({isSmall:!0,showTooltip:!0,label:t,icon:Ms,ref:n,className:"llms-drag-handle"},r))};function As(e){var t=e.id,n=e.index,r=e.item,o=e.isDragging,i=e.dragHandle,l=e.ListItem,s=e.itemClassName,a=void 0===s?"":s,c=e.manageState,u=e.extraProps,f=void 0===u?{}:u,d=Ps({id:t}),p=d.attributes,m=d.listeners,b=d.setNodeRef,h=d.transform,v=d.transition,g={transform:Qi.Transform.toString(h),transition:v};return o&&h&&h.scaleX&&h.scaleY&&(h.scaleX=.9,h.scaleY=.9),o&&(a+=" llms-is-dragging"),Object(z.createElement)("div",Tt()({style:g,ref:i?void 0:b,className:"llms-sortable-list--item ".concat(a)},p,i?{}:m),Object(z.createElement)(l,{id:t,item:r,index:n,isDragging:o,setNodeRef:b,listeners:m,manageState:c,extraProps:f}))}var Ns=function(e){var t=e.ListItem,n=e.manageState,r=e.items,o=void 0===r?[]:r,i=e.sortableStrategy,l=void 0===i?js:i,s=e.ctxModifiers,a=void 0===s?[Rs,Ts]:s,c=e.dragHandle,u=void 0===c||c,f=e.listClassName,d=void 0===f?"":f,p=e.itemClassName,m=void 0===p?"":p,b=e.extraProps,h=void 0===b?{}:b,v=Object(z.useState)(!1),g=Di()(v,2),y=g[0],O=g[1],_=function(...e){return Object(me.useMemo)(()=>[...e].filter(e=>null!=e),[...e])}(Xl(as),Xl(ns,{coordinateGetter:Ds}));return Object(z.createElement)(bs,{sensors:_,collisionDetection:ml,onDragStart:function(e){O(e.active.id)},onDragEnd:function(e){O(!1);var t=e.active,r=e.over;if(t.id!==r.id){var i=Object(Hr.findIndex)(o,{id:t.id}),l=Object(Hr.findIndex)(o,{id:r.id});n.updateItems(gs(o,i,l))}},modifiers:a},Object(z.createElement)("div",{className:"llms-sortable-list ".concat(d)},Object(z.createElement)(xs,{items:o,strategy:l},o.map((function(e,r){return Object(z.createElement)(As,{id:e.id,key:e.id,index:r,item:e,isDragging:e.id===y,dragHandle:u,ListItem:t,itemClassName:m,manageState:n,extraProps:h})})))))};function Fs(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Bs(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Fs(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Fs(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function Vs(e){var t=e.id,n=e.item,r=e.extraProps,o=e.manageState,i=e.listeners,l=e.setNodeRef,s=r.showKeys,a=r.type,c=r.optionCount,u=o.updateItem,f=o.deleteItem;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(Ls,{label:Object($.__)("Reorder option","lifterlms"),setNodeRef:l,listeners:i}),Object(z.createElement)(K.Tooltip,{text:Object($.__)("Make default","lifterlms")},Object(z.createElement)("div",{className:"llms-field-opt-default-wrap"},"checkbox"===a&&Object(z.createElement)((function(){return Object(z.createElement)(K.CheckboxControl,{className:"llms-field-opt-default",checked:"yes"===n.default,onChange:function(e){u(t,Bs(Bs({},n),{},{default:!0===e?"yes":"no"}))},tabIndex:"-1"})}),null),"checkbox"!==a&&Object(z.createElement)((function(){return Object(z.createElement)(K.RadioControl,{className:"llms-field-opt-default",selected:n.default,onChange:function(e){u(t,Bs(Bs({},n),{},{default:e}))},options:[{label:"",value:"yes"}],tabIndex:"-1"})}),null))),Object(z.createElement)("div",{className:"llms-field-opt-text-wrap"},Object(z.createElement)(K.TextControl,{className:"llms-field-opt-text",value:n.text,onChange:function(e){return u(t,Bs(Bs({},n),{},{text:e}))},placeholder:Object($.__)("Option label","lifterlms")}),s&&Object(z.createElement)("div",{className:"llms-field-opt-db-key"},Object(z.createElement)(K.Tooltip,{text:Object($.__)("Database key value","lifterlms")},Object(z.createElement)(K.Dashicon,{icon:"database"})),Object(z.createElement)(K.TextControl,{className:"llms-field-opt-text ",value:n.key,onChange:function(e){return u(t,Bs(Bs({},n),{},{key:e}))},placeholder:Object($.__)("Database key value","lifterlms")}))),c>1&&Object(z.createElement)("div",{className:"llms-del-field-opt-wrap"},Object(z.createElement)(K.Button,{style:{flex:1},icon:"trash",label:Object($.__)("Delete Option","lifterlms"),onClick:function(){return f(t)},tabIndex:"-1",isSmall:!0})))}var Us=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o),e=r.apply(this,arguments),U()(ce()(e),"addOption",(function(){var t=e.state.options,n=t.length,r=e.getUniqueKeyNumber(n+1),o=Di()(r,2),i=o[0],l=o[1],s={key:i,id:Fi(), +// Translators: %d = Option index in the list of options. +text:Object($.sprintf)(Object($.__)("Option %d","lifterlms"),l),default:"no"};t.push(s),e.updateOptions(t)})),U()(ce()(e),"getManageState",(function(){return{createItem:e.addOption,deleteItem:e.removeOption,updateItem:e.updateOption,updateItems:e.updateOptions}})),U()(ce()(e),"getUniqueKeyNumber",(function(t){for(var n=function(t){return-1===e.state.options.findIndex((function(e){return e.key===t}))},r=Object($.sprintf)(Object($.__)("option_%d","lifterlms"),t)// Translators: %d = Option index in the list of options. +;!n(r);){var o=e.getUniqueKeyNumber(++t),i=Di()(o,2);r=i[0],t=i[1]}return[r,t]})),U()(ce()(e),"updateOption",(function(t,n){var r=e.state.options,o=e.props.attributes.field,i="yes"===n.default&&"checkbox"!==o,l=r.map((function(e){return t===e.id?e=Bs(Bs({},e),n):i&&(e=Bs(Bs({},e),{},{default:"no"})),e}));e.updateOptions(l)})),U()(ce()(e),"updateOptions",(function(t){var n=e.props.setAttributes;e.setState({options:t}),n({options:t.map((function(e){return e.id,Pi()(e,["id"])}))})})),U()(ce()(e),"removeOption",(function(t){var n=e.state.options,r=e.props.attributes.field,o=null;if("checkbox"!==r){var i=n.find((function(e){return e.id===t}));o="yes"===i.default}e.updateOptions(n.filter((function(e){return e.id!==t})).map((function(e,t){return o&&0===t&&(e=Bs(Bs({},e),{},{default:"yes"})),e})))}));var t=e.props.attributes.options;return e.state={showKeys:!1,options:t.map((function(e){return Bs(Bs({},e),{},{id:Fi()})}))},e}return J()(o,[{key:"render",value:function(){var e=this,t=this.props,n=this.state,r=t.attributes,o=r.id,i=r.field,l=n.options,s=n.showKeys,a=l.length;return Object(z.createElement)(K.BaseControl,{id:o,label:Object($.__)("Options","lifterlms")},Object(z.createElement)(Ns,{ListItem:Vs,items:l,itemClassName:"llms-field-option",manageState:this.getManageState(),extraProps:{type:i,showKeys:s,optionCount:a}}),Object(z.createElement)("div",{className:"llms-field-options--footer"},Object(z.createElement)(K.Button,{isSecondary:!0,onClick:this.addOption},Object($.__)("Add option","lifterlms")),Object(z.createElement)(K.Button,{isTertiary:!0,onClick:function(){return e.setState({showKeys:!s})}},s?Object($.__)("Hide keys","lifterlms"):Object($.__)("Show keys","lifterlms"))))}}]),o}(z.Component);function Hs(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function qs(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Hs(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Hs(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var zs=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(e){var t;return X()(this,o),(t=r.call(this,e)).state={validationErrors:{}},t}return J()(o,[{key:"getBlockByFieldId",value:function(e){var t=Zr().filter((function(t){return e===t.attributes.id}));return!!t&&t[0]}},{key:"getColumnsOptions",value:function(e){var t=[];return e&&(!e||e["llms/fieldGroup/fieldLayout"]&&"stacked"!==e["llms/fieldGroup/fieldLayout"])||t.push({value:12,label:Object($.__)("100%","lifterlms")}),t.concat([{value:9,label:Object($.__)("75%","lifterlms")},{value:8,label:Object($.__)("66.66%","lifterlms")},{value:6,label:Object($.__)("50%","lifterlms")},{value:4,label:Object($.__)("33.33%","lifterlms")},{value:3,label:Object($.__)("25%","lifterlms")}])}},{key:"getMatchFieldOptions",value:function(){var e=this.props,t=e.clientId,n=e.name;return[{value:"",label:Object($.__)("Select a field","lifterlms")}].concat(Zr().filter((function(e){return e.clientId!==t&&-1!==n.indexOf("llms/form-field-")})).map((function(e){var t=e.attributes,n=t.id,r=t.label;return{value:n,label:"".concat(r," (").concat(n,")")}})))}},{key:"hasInspectorSupport",value:function(){var e=this.props.inspectorSupports;return Object.keys(e).filter((function(t){return e[t]})).length>=1}},{key:"hasInspectorControlSupport",value:function(e){return this.props.inspectorSupports[e]}},{key:"canTransformToGroup",value:function(e){return!(!e||this.isInAConfirmGroup(e))&&Object(Jr.getPossibleBlockTransformations)([e]).map((function(e){return e.name})).includes("llms/form-field-confirm-group")}},{key:"isInAConfirmGroup",value:function(e){return!!this.getParentGroupClientId(e)}},{key:"getParentGroupClientId",value:function(e){if(!e)return!1;var t=e.clientId,n=(0,Object(Xr.select)(G.store).getBlockParentsByBlockName)(t,"llms/form-field-confirm-group");return!!n.length&&n[0]}},{key:"getBlockSiblings",value:function(e){var t=this.getParentGroupClientId(e);return t?(0,Object(Xr.select)(G.store).getBlock)(t).innerBlocks.filter((function(t){return t.clientId!==e.clientId})):[]}},{key:"getValidationErrText",value:function(e){var t="",n=this.state.validationErrors[e];if(n)if(this.containsInvalidCharacters(n)) +// Translators: %s = user-submitted value. +t=Object($.__)('The value "%s" contains invalid characters. Only letters, numbers, underscores, and hyphens are allowed.',"lifterlms");else switch(e){case"data_store_key": +// Translators: %s = user-submitted value. +t=Object($.__)('The user meta key "%s" is not unique. Please choose a unique value.',"lifterlms");break;case"id": +// Translators: %s = user-submitted value. +t=Object($.__)('The ID "%s" is not unique. Please choose a unique field ID.',"lifterlms");break;case"name": +// Translators: %s = user-submitted value. +t=Object($.__)('The name "%s" is not unique. Please choose a globally unique field name.',"lifterlms");break;default: +// Translators: %s = user-submitted value. +t=Object($.__)('The chosen value "%s" is invalid.',"lifterlms")}else t=Object($.__)("The value cannot be blank.","lifterlms");return Object($.sprintf)(t,n)}},{key:"containsInvalidCharacters",value:function(e){return!!e.match(/[^A-Za-z0-9\-\_]/g)}},{key:"setValidationError",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;this.setState({validationErrors:qs(qs({},this.state.validationErrors),{},U()({},e,t))})}},{key:"hasValidationErr",value:function(e){return"string"==typeof this.state.validationErrors[e]}},{key:"ValidatedTextControl",value:function(e){var t=e.parent,n=e.attrKey,r=e.label,o=e.help,i=t.props.attributes[n],l=t.hasValidationErr(n),s=l?"llms-invalid-control":"";return Object(z.createElement)("div",{className:s},Object(z.createElement)(K.TextControl,{label:r,help:o,value:i,onChange:function(e){return t.updateValueWithValidation(n,e,"name"===n?"global":"local")}}),l&&Object(z.createElement)("p",{className:"llms-invalid-control--msg"},t.getValidationErrText(n)))}},{key:"updateValueWithValidation",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"local",r=this.props,o=r.clientId,i=r.attributes,l=r.setAttributes,s=i.name,a=i[e],c=Object(Xr.dispatch)(Lo),u=c.editField,f=c.renameField,d=Object(Xr.dispatch)(Qr.store),p=d.lockPostSaving,m=d.unlockPostSaving,b="llms-".concat(e,"-validation-err-").concat(o,"-").concat(s);if(t!==a){var h=!t,v=this.containsInvalidCharacters(t),g=wi(t,e,n),y=!h&&!v&&g;if(this.setValidationError(e),m(b),!y){if(this.setValidationError(e,t),h)return;p(b)}"name"===e?(g||(t=t.slice(0,-1)),f(i.name,t)):u(i.name,U()({},e,t)),l(U()({},e,t))}}},{key:"render",value:function(){var e=this;if(!this.hasInspectorSupport())return"";var t=this.props,n=t.attributes,r=t.setAttributes,o=t.clientId,i=t.context,l=Object(Xr.select)(G.store).getBlock(o),s=n.required,a=n.placeholder,c=n.columns,u=n.isConfirmationField,f=n.isConfirmationControlField,d=this.canTransformToGroup(l),p=this.isInAConfirmGroup(l);return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(G.InspectorControls,null,Object(z.createElement)(K.PanelBody,null,!u&&this.hasInspectorControlSupport("required")&&Object(z.createElement)(K.ToggleControl,{className:"llms-required-field-toggle",label:Object($.__)("Required","lifterlms"),checked:!!s,onChange:function(){return r({required:!s})},help:s?Object($.__)("Field is required.","lifterlms"):Object($.__)("Field is optional.","lifterlms")}),Object(z.createElement)(K.SelectControl,{className:"llms-field-width-select",label:Object($.__)("Field Width","lifterlms"),onChange:function(t){t=parseInt(t,10),r({columns:t});var n=e.getBlockSiblings(l);n.length&&t+n[0].attributes.columns>12&&(0,Object(Xr.dispatch)(G.store).updateBlockAttributes)(n[0].clientId,{columns:12-t})},help:Object($.__)("Determines the width of the form field.","lifterlms"),value:c,options:this.getColumnsOptions(i)}),this.hasInspectorControlSupport("options")&&Object(z.createElement)(Us,{attributes:n,setAttributes:r}),this.hasInspectorControlSupport("placeholder")&&Object(z.createElement)(K.TextControl,{label:Object($.__)("Placeholder","lifterlms"),value:a,onChange:function(e){return r({placeholder:e})},help:Object($.__)("Displays a placeholder option as the selected instead of a default value.","lifterlms")}),(d||f&&p)&&Object(z.createElement)(K.ToggleControl,{className:"llms-confirmation-field-toggle",label:Object($.__)("Confirmation Field","lifterlms"),checked:p,onChange:function(){var t=Object(Xr.dispatch)(G.store),n=t.replaceBlock,r=t.selectBlock,i=Object(Jr.getBlockType)("llms/form-field-confirm-group").findControllerBlockIndex,s=Object(Xr.select)(G.store).getBlock,a=o,c="llms/form-field-confirm-group",u=l,f=null;p&&(u=s(a=e.getParentGroupClientId(l)),c=l.name);var d=Object(Jr.switchToBlockType)(u,c);if(n(a,d),p)f=d[0].clientId;else{var m=d[0].innerBlocks;f=m[i(m)].clientId}r(f)},help:p?Object($.__)("A Confirmation field is active.","lifterlms"):Object($.__)("No confirmation field.","lifterlms")}),this.hasInspectorControlSupport("customFill")&&Object(z.createElement)(K.Slot,{name:"llmsInspectorControlsFill.".concat(this.hasInspectorControlSupport("customFill"),".").concat(o)})),!u&&this.hasInspectorControlSupport("storage")&&Object(z.createElement)(K.PanelBody,{title:Object($.__)("Data Storage","lifterlms")},Object(z.createElement)(this.ValidatedTextControl,{parent:this,attrKey:"data_store_key",label:Object($.__)("Usermeta Key","lifterlms"),help:Object($.__)("Database field key name. Only accepts alphanumeric characters, hyphens, and underscores.","lifterlms")}))),Object(z.createElement)(G.InspectorAdvancedControls,null,!u&&this.hasInspectorControlSupport("name")&&Object(z.createElement)(this.ValidatedTextControl,{parent:this,attrKey:"name",label:Object($.__)("Field Name","lifterlms"),help:Object($.__)("The field's HTML name attribute.","lifterlms")}),!u&&this.hasInspectorControlSupport("id")&&Object(z.createElement)(this.ValidatedTextControl,{parent:this,attrKey:"id",label:Object($.__)("Field ID","lifterlms"),help:Object($.__)("The field's HTML id attribute.","lifterlms")})))}}]),o}(z.Component);function $s(e){var t=function(e){var t,n,r,o=Object(Xr.select)("core/block-editor");return(0,o.getBlock)((0,o.getBlockParentsByBlockName)(e,(t=Object(Xr.select)("core/blocks"),n=t.getBlockTypes,r=t.hasBlockSupport,n().filter((function(e){return r(e,"llms_field_group")}))).map((function(e){return e.name}))))}(e);return t&&t.innerBlocks.length?Object(Hr.find)(t.innerBlocks,(function(t){return t.clientId!==e})):null}function Ws(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.setAttributes,n=e.currentUpdates,r=e.siblingClientId,o=e.siblingUpdates,i=Object(Xr.dispatch)("core/block-editor"),l=i.updateBlockAttributes;setTimeout((function(){Object(Hr.isEmpty)(n)||t(n),r&&!Object(Hr.isEmpty)(o)&&l(r,o)}))}function Gs(e,t){var n={};return e.required!==t.required&&(n.required=e.required),e.field!==t.field&&(n.field=e.field),{currentUpdates:{},siblingUpdates:n}}var Ks=function(e){var t=e.attributes,n=e.block,r=e.setAttributes,o=t.fieldLayout,i=n.innerBlocks;return Object(z.createElement)(K.RadioControl,{label:Object($.__)("Field Layout","lifterlms"),selected:o,onChange:function(e){return function(e){var t=e.fieldLayout,n=e.setAttributes,r=e.innerBlocks,o=Object(Xr.dispatch)(G.store).updateBlockAttributes;n({fieldLayout:t});var i="columns"===t?6:12;r.forEach((function(e,n){var r=e.clientId,l=1===n;0===n&&"stacked"===t&&(l=!0),o(r,{columns:i,last_column:l})}))}({fieldLayout:e,setAttributes:r,innerBlocks:i})},options:[{value:"columns",label:Object($.__)("Columns","lifterlms")},{value:"stacked",label:Object($.__)("Stacked","lifterlms")}]})};function Ys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Xs(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Ys(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Ys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var Qs=function(e){var t=Object(Xr.select)("core/editor").getCurrentPostId;return Object(Hr.snakeCase)(Object(Hr.uniqueId)("".concat(e,"_").concat(t(),"_")))},Js=function(e){return Object(Hr.kebabCase)(e)},Zs={apiVersion:2,icon:{foreground:"#466dd8"},category:"llms-user-info-fields",keywords:[Object($.__)("LifterLMS","lifterlms"),"llms"],attributes:{},supports:{llms_visibility:!0},example:{},fillInspectorControls:function(e,t,n){},fillEditAfter:function(e,t,n){}},ea={attributes:{description:{type:"string",__default:""},field:{type:"string",__default:"text"},required:{type:"boolean",__default:!1},label:{type:"string",__default:""},label_show_empty:{type:"string",__default:!1},match:{type:"string",__default:""},options:{type:"array",__default:[]},options_preset:{type:"string",__default:""},placeholder:{type:"string",__default:""},columns:{type:"integer",__default:12},last_column:{type:"boolean",__default:!0},name:{type:"string",__default:""},id:{type:"string",__default:""},data_store:{type:"string",__default:"usermeta"},data_store_key:{type:"string",__default:""},html_attrs:{type:"object",__default:{}},isConfirmationField:{type:"boolean",__default:!1},isConfirmationControlField:{type:"boolean",__default:!1}},supports:{llms_field_inspector:{id:!0,name:!0,options:!1,placeholder:!1,required:!0,customFill:!1,storage:!0},llms_edit_fill:{after:!1},llms_field_group:!1},edit:function(e){var t=e.attributes,n=!0,r=e.name,o=Object(Jr.getBlockType)(r),i=e.clientId,l=e.context,s=e.setAttributes,a=o.supports.llms_field_inspector,c=o.supports.llms_edit_fill,u=o.fillEditAfter,f=o.fillInspectorControls,d=Object(Xr.select)(G.store).getSelectedBlockClientId,p=Object(Xr.select)(Lo).isDuplicate,m=!!l["llms/fieldGroup/fieldLayout"],b=t.name&&p(t.name,i),h=!m&&t.isConfirmationField;h&&(n=!1),t=n?function(e,t,n){if(Object.keys(t).forEach((function(n){var r=t[n].__default;void 0!==r&&void 0===e[n]&&(e[n]=r)})),!e.name||n&&!e.isConfirmationField){for(var r=Qs(e.field);!wi("name",r);)r=Qs(e.field);e.name=r}if(!e.id||n&&!e.isConfirmationField){for(var o=Js(e.name);!wi("id",o,"local");)o=Js(Object(Hr.uniqueId)("".concat(e.field,"-field-")));e.id=o}return(""===e.data_store_key||n&&!e.isConfirmationField)&&(e.data_store_key=e.name),e}(t,o.attributes,b):t,m&&n&&function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.attributes,n=e.clientId,r=e.setAttributes,o=$s(n),i={},l={};if(o){var s=o.clientId;if(t.isConfirmationControlField||t.isConfirmationField){var a=Gs(t,o.attributes);i=Object(Hr.merge)(i,a.currentUpdates),l=Object(Hr.merge)(l,a.siblingUpdates)}Ws({setAttributes:r,currentUpdates:i,siblingClientId:s,siblingUpdates:l})}}(e);var v=Object(G.useBlockProps)({className:"llms-fields llms-cols-".concat(t.columns)});return Object(z.useEffect)((function(){if(o.variations&&o.variations.length&&i===d())var e=setInterval((function(){var n=document.querySelector(".block-editor-block-inspector .block-editor-block-variation-transforms");return n&&(n.style.display=t.isConfirmationField?"none":"inline-block",clearInterval(e)),function(){clearInterval(e)}}),10)})),h?(setTimeout((function(){Object(Xr.dispatch)(G.store).removeBlock(i)}),10),null):Object(z.createElement)("div",v,Object(z.createElement)(zs,{attributes:t,clientId:i,name:r,setAttributes:s,inspectorSupports:a,context:l}),Object(z.createElement)(Ci,{attributes:t,setAttributes:s,block:o,clientId:i,context:l}),a.customFill&&Object(z.createElement)(K.Fill,{name:"llmsInspectorControlsFill.".concat(a.customFill,".").concat(i)},f(t,s,e)),c.after&&Object(z.createElement)(K.Fill,{name:"llmsEditFill.after.".concat(c.after,".").concat(i)},u(t,s,e)))},save:function(e){return e.attributes}},ta={attributes:{fieldLayout:{type:"string",default:"columns"}},supports:{llms_field_group:!0,llms_field_inspector:!1},providesContext:{"llms/fieldGroup/fieldLayout":"fieldLayout"},llmsInnerBlocks:{template:[],allowed:[],lock:"insert"},edit:function(e){var t=e.attributes,n=e.clientId,r=e.name,o=e.setAttributes,i=t.fieldLayout,l=(0,Object(Xr.select)(G.store).getBlock)(n),s=Object(Jr.getBlockType)(r),a=s.llmsInnerBlocks,c=a.allowed,u=a.template,f=a.lock,d=l&&l.innerBlocks.length&&"llms/form-field-confirm-group"===l.name?l.innerBlocks[s.findControllerBlockIndex(l.innerBlocks)]:null,p=d?Object(Jr.getBlockType)(d.name):null,m=p?p.supports.llms_edit_fill:{after:!1},b=s.supports.llms_field_inspector,h=s.providesContext&&s.providesContext["llms/fieldGroup/fieldLayout"],v="columns"===i?"horizontal":"vertical";return h||(v="vertical"),Object(z.createElement)("div",Object(G.useBlockProps)(),Object(z.createElement)(G.InspectorControls,null,Object(z.createElement)(K.PanelBody,null,h&&Object(z.createElement)(Ks,Xs(Xs({},e),{},{block:l})),b.customFill&&s.fillInspectorControls(t,o,e))),Object(z.createElement)("div",{className:"llms-field-group","data-field-layout":h?i:"stacked"},Object(z.createElement)(G.InnerBlocks,{allowedBlocks:c,template:"function"==typeof u?u({attributes:t,clientId:n,block:l,blockType:s}):u,templateLock:f,orientation:v})),m.after&&Object(z.createElement)(K.Slot,{name:"llmsEditFill.after.".concat(m.after,".").concat(d.clientId)}))},save:function(){return Object(z.createElement)(G.InnerBlocks.Content,null)}},na=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"field",t="field"===e?ea:ta;return Object(Hr.merge)({},Object(Hr.cloneDeep)(Zs),t)};function ra(){return Object(Hr.cloneDeep)(["llms_form","wp_block"])}function oa(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[];e=Object(Hr.cloneDeep)(e);for(var r=0;r<n.length;r++)delete e[n[r]];return Object(Hr.merge)({},e,t)}function ia(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:2,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=[],r=1;r<=e;r++)n.push({default:t&&t>0?"yes":"no", +// Translators: %d = Option index in the list of options. +text:Object($.sprintf)(Object($.__)("Option %d","lifterlms"),r), +// Translators: %d = Option index in the list of options. +key:Object($.sprintf)(Object($.__)("option_%d","lifterlms"),r)}),t--;return n}function la(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function sa(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?la(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):la(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var aa="llms/form-field-confirm-group",ca=ra(),ua=!0;function fa(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.id,n=e.match;return t&&!n&&(n="".concat(t,"_confirm")),sa(sa({},e),{},{match:n,columns:6,last_column:!1,isConfirmationControlField:!0,llms_visibility:"off"})}function da(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.id,n=e.name,r=e.match;return t&&!r&&(r=t,t="".concat(t,"_confirm"),n="".concat(n,"_confirm")),sa(sa({},e),{},{id:t,name:n,match:r,label:e.label?// Translators: %s label of the controller field. +Object($.sprintf)(Object($.__)("Confirm %s","lifterlms"),e.label):"",columns:6,last_column:!0,data_store:!1,data_store_key:!1,isConfirmationField:!0,llms_visibility:"off"})}function pa(e){(0,Object(Xr.dispatch)(Lo).unloadField)(e)}var ma=["llms/form-field-text","llms/form-field-user-email","llms/form-field-user-login","llms/form-field-user-password"],ba={from:[],to:[]};ma.forEach((function(e){ba.from.push({type:"block",blocks:[e],transform:function(t){pa(t.name);var n=t.llms_visibility,r=fa(t),o=da(t),i=[Object(Jr.createBlock)(e,r),Object(Jr.createBlock)("llms/form-field-text",o)];return Object(Jr.createBlock)(aa,{llms_visibility:n},Object($.isRTL)()?i.reverse():i)}}),ba.to.push({type:"block",blocks:[e],isMatch:function(){var t=(0,Object(Xr.select)(G.store).getSelectedBlock)().innerBlocks;return(t[ha(t)]||{}).name===e},transform:function(e,t){var n=e.llms_visibility,r=t[ha(t)],o=r.name,i=r.attributes;return pa(i.name),Object(Jr.createBlock)(o,sa(sa({},i),{},{columns:12,last_column:!0,isConfirmationControlField:!1,match:"",llms_visibility:n}))}})}));var ha=function(e){return e.findIndex((function(e){return e.attributes.isConfirmationControlField}))},va=oa(na("group"),{title:Object($.__)("Input Confirmation Group","lifterlms"),description:Object($.__)("Adds a required confirmation field to an input field.","lifterlms"),icon:{src:"controls-repeat"},category:"llms-custom-fields",transforms:ba,fillInspectorControls:function(e,t,n){var r=n.clientId;return Object(z.createElement)(K.Button,{isDestructive:!0,onClick:function(){return function(e){var t=Object(Xr.select)(G.store).getBlock,n=Object(Xr.dispatch)(G.store).replaceBlock,r=t(e),o=r.innerBlocks,i=r.attributes.llms_visibility,l=o[ha(o)],s=l.name,a=l.attributes;pa(a.name),n(e,Object(Jr.createBlock)(s,sa(sa({},a),{},{columns:12,last_column:!0,isConfirmationControlField:!1,match:"",llms_visibility:i})))}(r)}},Object($.__)("Remove confirmation field","lifterlms"))},findControllerBlockIndex:ha,supports:{llms_field_inspector:{customFill:"confirmGroupAdditionalControls"},inserter:!1},llmsInnerBlocks:{allowed:ma,template:function(e){var t=e.block,n=null;return t&&t.innerBlocks.length||(n=[["llms/form-field-text",fa()],["llms/form-field-text",da()]]),n}}}),ga=Object(z.createElement)("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20px",height:"20px",viewBox:"0 0 416 448"},Object(z.createElement)("path",{d:"M352 232.5v79.5q0 29.75-21.125 50.875t-50.875 21.125h-208q-29.75 0-50.875-21.125t-21.125-50.875v-208q0-29.75 21.125-50.875t50.875-21.125h208q15.75 0 29.25 6.25 3.75 1.75 4.5 5.75 0.75 4.25-2.25 7.25l-12.25 12.25q-2.5 2.5-5.75 2.5-0.75 0-2.25-0.5-5.75-1.5-11.25-1.5h-208q-16.5 0-28.25 11.75t-11.75 28.25v208q0 16.5 11.75 28.25t28.25 11.75h208q16.5 0 28.25-11.75t11.75-28.25v-63.5q0-3.25 2.25-5.5l16-16q2.5-2.5 5.75-2.5 1.5 0 3 0.75 5 2 5 7.25zM409.75 110.25l-203.5 203.5q-6 6-14.25 6t-14.25-6l-107.5-107.5q-6-6-6-14.25t6-14.25l27.5-27.5q6-6 14.25-6t14.25 6l65.75 65.75 161.75-161.75q6-6 14.25-6t14.25 6l27.5 27.5q6 6 6 14.25t-6 14.25z"}));function ya(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Oa(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ya(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ya(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var _a="llms/form-field-checkboxes",ja=ra(),wa=!1,xa=oa(na(),{title:Object($.__)("Checkboxes","lifterlms"),description:Object($.__)("A single checkbox toggle or a group of multiple checkboxes.","lifterlms"),icon:{src:ga},category:"llms-custom-fields",supports:{llms_field_inspector:{options:!0}},attributes:{field:{__default:"checkbox"},options:{__default:ia(2,0)}},transforms:{from:[{type:"block",blocks:["llms/form-field-radio","llms/form-field-select"],transform:function(e){return Object(Jr.createBlock)(_a,Oa(Oa({},e),{},{field:xa.attributes.field.__default}))}}]}}),Ea=Object(z.createElement)("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20px",height:"20px",viewBox:"0 0 768 768"},Object(z.createElement)("path",{d:"M384 640.5c141 0 256.5-115.5 256.5-256.5s-115.5-256.5-256.5-256.5-256.5 115.5-256.5 256.5 115.5 256.5 256.5 256.5zM384 64.5c177 0 319.5 142.5 319.5 319.5s-142.5 319.5-319.5 319.5-319.5-142.5-319.5-319.5 142.5-319.5 319.5-319.5zM384 223.5c88.5 0 160.5 72 160.5 160.5s-72 160.5-160.5 160.5-160.5-72-160.5-160.5 72-160.5 160.5-160.5z"}));function ka(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Ca(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ka(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ka(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var Sa="llms/form-field-radio",Pa=ra(),Ia=!1,Da=oa(xa,{title:Object($.__)("Radio","lifterlms"),description:Object($.__)("A group of radio inputs which can be populated with any number of options.","lifterlms"),icon:{src:Ea},attributes:{field:{__default:"radio"},options:{__default:ia(2,1)}},transforms:{from:[{type:"block",blocks:["llms/form-field-checkboxes","llms/form-field-select"],transform:function(e){return Object(Jr.createBlock)(Sa,Ca(Ca({},e),{},{field:Da.attributes.field.__default}))}}]}}),Ra=Object(z.createElement)("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20px",height:"20px",viewBox:"0 0 384 448"},Object(z.createElement)("path",{d:"M286.25 168.75q4.5 8.75-1.25 16.5l-80 112q-4.75 6.75-13 6.75t-13-6.75l-80-112q-5.75-7.75-1.25-16.5 4.25-8.75 14.25-8.75h160q10 0 14.25 8.75zM320 344v-240q0-3.25-2.375-5.625t-5.625-2.375h-240q-3.25 0-5.625 2.375t-2.375 5.625v240q0 3.25 2.375 5.625t5.625 2.375h240q3.25 0 5.625-2.375t2.375-5.625zM384 104v240q0 29.75-21.125 50.875t-50.875 21.125h-240q-29.75 0-50.875-21.125t-21.125-50.875v-240q0-29.75 21.125-50.875t50.875-21.125h240q29.75 0 50.875 21.125t21.125 50.875z"}));function Ta(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Ma(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Ta(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Ta(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var La="llms/form-field-select",Aa=ra(),Na=!1,Fa=oa(Da,{title:Object($.__)("Dropdown","lifterlms"),description:Object($.__)("A select field which can be populated with any number of options.","lifterlms"),icon:{src:Ra},attributes:{field:{__default:"select"}},supports:{llms_field_inspector:{placeholder:!0}},transforms:{from:[{type:"block",blocks:["llms/form-field-checkboxes","llms/form-field-radio"],transform:function(e){return Object(Jr.createBlock)(La,Ma(Ma({},e),{},{field:Fa.attributes.field.__default}))}}]}}),Ba=Object(z.createElement)("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20px",height:"20px",viewBox:"0 0 512 512"},Object(z.createElement)("path",{d:"M448 0h-384c-8.832 0-16 7.168-16 16v96c0 8.832 7.168 16 16 16h16c8.832 0 16-7.168 16-16l32-48h96v384l-80 32c-8.832 0-16 7.152-16 16s7.168 16 16 16h224c8.848 0 16-7.152 16-16s-7.152-16-16-16l-80-32v-384h96l32 48c0 8.832 7.152 16 16 16h16c8.848 0 16-7.168 16-16v-96c0-8.832-7.152-16-16-16z"})),Va=Object(z.createElement)("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20px",height:"20px",viewBox:"0 0 540 540"},Object(z.createElement)("path",{d:"M247.75 256l16-64h-63.5l-16 64h63.5zM439.75 130l-14 56q-1.75 6-7.75 6h-81.75l-16 64h77.75q3.75 0 6.25 3 2.5 3.5 1.5 7l-14 56q-1.25 6-7.75 6h-81.75l-20.25 82q-1.75 6-7.75 6h-56q-4 0-6.5-3-2.25-3-1.5-7l19.5-78h-63.5l-20.25 82q-1.75 6-7.75 6h-56.25q-3.75 0-6.25-3-2.25-3-1.5-7l19.5-78h-77.75q-3.75 0-6.25-3-2.25-3-1.5-7l14-56q1.75-6 7.75-6h81.75l16-64h-77.75q-3.75 0-6.25-3-2.5-3.5-1.5-7l14-56q1.25-6 7.75-6h81.75l20.25-82q1.75-6 8-6h56q3.75 0 6.25 3 2.25 3 1.5 7l-19.5 78h63.5l20.25-82q1.75-6 8-6h56q3.75 0 6.25 3 2.25 3 1.5 7l-19.5 78h77.75q3.75 0 6.25 3 2.25 3 1.5 7z"}));function Ua(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Ha(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Ua(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Ua(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var qa=na(),za="llms/form-field-text",$a=ra(),Wa=!0,Ga=[{name:"text",title:Object($.__)("Text","lifterlms"),description:Object($.__)("An input field which accepts any form of text.","lifterlms"),isDefault:!0,icon:Ba},{name:"email",title:Object($.__)("Email","lifterlms"),description:Object($.__)("A text input field which only accepts an email address.","lifterlms"),icon:"email-alt"},{name:"password",title:Object($.__)("Password","lifterlms"),description:Object($.__)("User password confirmation field.","lifterlms"),icon:"lock",scope:[]},{name:"number",title:Object($.__)("Number","lifterlms"),description:Object($.__)("An input field which only accepts numeric input.","lifterlms"),icon:Va,attributes:{html_attrs:{min:"",max:""}}},{name:"tel",title:Object($.__)("Phone Number","lifterlms"),description:Object($.__)("An input field which only accepts phone numbers.","lifterlms"),icon:"phone"},{name:"url",title:Object($.__)("Website Address / URL","lifterlms"),description:Object($.__)("An input field which only accepts a website address or URL.","lifterlms"),icon:"admin-links"}];Ga.forEach((function(e){e.scope=e.scope||["block","inserter","transform"],e.icon=Ha(Ha({},qa.icon),{},{src:e.icon}),e.attributes||(e.attributes={}),e.attributes.field=e.name,e.isActive=function(e,t){return e.field===t.field}}));var Ka=oa(qa,{title:Object($.__)("Text","lifterlms"),description:Object($.__)("A simple text input field.","lifterlms"),icon:{src:Ba},usesContext:["llms/fieldGroup/fieldLayout"],supports:{inserter:!1,llms_field_inspector:{customFill:"fieldTextAdditionalControls"}},variations:Ga,fillInspectorControls:function(e,t){if(!e.isConfirmationField&&"number"===e.field){var n=e.html_attrs,r=n.min,o=n.max;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(K.TextControl,{label:Object($.__)("Minimum Value","lifterlms"),help:Object($.__)("Specify the minimum allowed value. Leave blank for no minimum.","lifterlms"),value:r,type:"number",onChange:function(e){return t({html_attrs:Ha(Ha({},n),{},{min:e})})}}),Object(z.createElement)(K.TextControl,{label:Object($.__)("Maximum Value","lifterlms"),help:Object($.__)("Specify the maximum allowed value. Leave blank for no maximum.","lifterlms"),value:o,type:"number",onChange:function(e){return t({html_attrs:Ha(Ha({},n),{},{max:e})})}}))}}});function Ya(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Xa(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Ya(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Ya(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var Qa="llms/form-field-textarea",Ja=!1,Za=oa(Ka,{title:Object($.__)("Textarea","lifterlms"),description:Object($.__)("A text field accepting multiple lines of user information.","lifterlms"),icon:{src:"editor-paragraph"},category:"llms-custom-fields",supports:{inserter:!0,llms_field_inspector:{customFill:"fieldTextarea"}},attributes:{field:{__default:"textarea"},html_attrs:{__default:{rows:4}}},fillInspectorControls:function(e,t){var n=e.html_attrs,r=n.rows;return Object(z.createElement)(K.TextControl,{label:Object($.__)("Rows","lifterlms"),help:Object($.__)("Specify the number of text rows for the textarea input.","lifterlms"),value:r,type:"number",onChange:function(e){return t({html_attrs:Xa(Xa({},n),{},{rows:e})})},min:"2",step:"1"})},transforms:{from:[{type:"block",blocks:["llms/form-field-text"],transform:function(e){return Object(Jr.createBlock)(Qa,Xa(Xa({},e),{},{html_attrs:Xa(Xa({},e.html_attrs),{},{rows:4}),field:"textarea"}))}}],to:[{type:"block",blocks:["llms/form-field-text"],transform:function(e){return Object(Jr.createBlock)("llms/form-field-text",Xa(Xa({},e),{},{field:"text"}))}}]}},["transforms","variations"]),ec="llms/form-field-redeem-voucher",tc=!0,nc=oa(Ka,{title:Object($.__)("Voucher Code Redemption","lifterlms"),description:Object($.__)("Allows user to redeem a voucher code during account registration.","lifterlms"),icon:{src:"tickets-alt"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,storage:!1,customFill:"redeemVoucher"}},attributes:{id:{__default:"llms_voucher"},field:{__default:"text"},label:{__default:Object($.__)("Have a voucher?","lifterlms")},name:{__default:"llms_voucher"},placeholder:{__default:Object($.__)("Voucher Code","lifterlms")},data_store:{__default:!1},data_store_key:{__default:!1},toggleable:{__default:!1}},fillInspectorControls:function(e,t){var n=e.toggleable;return e.required?null:Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Toggleable","lifterlms"),checked:!!n,onChange:function(){return t({toggleable:!n})},help:n?Object($.__)("Field is revealed when the toggle is clicked.","lifterlms"):Object($.__)("Field is always visible.","lifterlms")})}},["transforms","variations"]),rc="llms/form-field-user-display-name",oc=!0,ic=oa(Ka,{title:Object($.__)("User Display Name","lifterlms"),description:Object($.__)("Allows a user to choose how their name will be displayed publicly on the site.","lifterlms"),icon:{src:"nametag"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!1,storage:!1}},attributes:{id:{__default:"display_name"},field:{__default:"text"},label:{__default:Object($.__)("Display Name","lifterlms")},name:{__default:"display_name"},required:{__default:!0},data_store:{__default:"users"},data_store_key:{__default:"display_name"}}},["transforms","variations"]),lc="llms/form-field-user-login",sc=!0,ac=oa(Ka,{title:Object($.__)("User Login","lifterlms"),description:Object($.__)("Field used to collect a user's account username. If this field is omitted a username will be automatically generated based off their email address. Users can always login using either their email address or username.","lifterlms"),icon:{src:"admin-users"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!1,storage:!1}},attributes:{id:{__default:"user_login"},field:{__default:"text"},label:{__default:Object($.__)("Username","lifterlms")},name:{__default:"user_login"},required:{__default:!0},data_store:{__default:"users"},data_store_key:{__default:"user_login"},llms_visibility:{default:"logged_out"}}},["transforms","variations"]),cc="llms/form-field-user-email",uc=!0,fc=oa(Ka,{title:Object($.__)("User Email","lifterlms"),description:Object($.__)("A special field used to collect a user's account email address.","lifterlms"),icon:{src:"email-alt"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!1,storage:!1}},attributes:{id:{__default:"email_address"},field:{__default:"email"},label:{__default:Object($.__)("Email Address","lifterlms")},name:{__default:"email_address"},required:{__default:!0},data_store:{__default:"users"},data_store_key:{__default:"user_email"}}},["transforms","variations"]),dc="llms/form-field-user-first-name",pc=!0,mc=oa(Ka,{title:Object($.__)("First Name","lifterlms"),description:Object($.__)("A special field used to collect a user's first name.","lifterlms"),icon:{src:"admin-users"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,storage:!1}},attributes:{id:{__default:"first_name"},field:{__default:"text"},label:{__default:Object($.__)("First Name","lifterlms")},name:{__default:"first_name"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"first_name"}},parent:["llms/form-field-user-name"],usesContext:["llms/fieldGroup/fieldLayout"]},["transforms","variations"]),bc="llms/form-field-user-last-name",hc=!0,vc=oa(mc,{title:Object($.__)("Last Name","lifterlms"),description:Object($.__)("A special field used to collect a user's last name.","lifterlms"),attributes:{id:{__default:"last_name"},label:{__default:Object($.__)("Last Name","lifterlms")},name:{__default:"last_name"},data_store_key:{__default:"last_name"}}}),gc="llms/form-field-user-name",yc=ra(),Oc=!0,_c=oa(na("group"),{title:Object($.__)("User name","lifterlms"),description:Object($.__)("A special field used to collect a user's first and last name.","lifterlms"),icon:{src:"admin-users"},supports:{inserter:!0,multiple:!1},llmsInnerBlocks:{allowed:["llms/form-field-user-first-name","llms/form-field-user-last-name"],template:[["llms/form-field-user-first-name",{columns:6,last_column:!1}],["llms/form-field-user-last-name",{columns:6,last_column:!0}]]}});function jc(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function wc(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?jc(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):jc(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var xc="llms/form-field-user-password",Ec=!0;function kc(e,t){return{html_attrs:wc(wc({},e),{},{minlength:t})}}var Cc=oa(Ka,{title:Object($.__)("User Password","lifterlms"),description:Object($.__)("A special field used to collect a user's account password.","lifterlms"),icon:{src:"lock"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!1,storage:!1,customFill:"userPassAdditionalControls"},llms_edit_fill:{after:"userPassStrengthMeter"}},attributes:{id:{__default:"password"},field:{__default:"password"},label:{__default:Object($.__)("Password","lifterlms")},name:{__default:"password"},required:{__default:!0},data_store:{__default:"users"},data_store_key:{__default:"user_pass"},meter:{type:"boolean",__default:!0},meter_description:{type:"string",__default:Object($.__)("A strong password is required with at least 8 characters. To make it stronger, use both upper and lower case letters, numbers, and symbols.","lifterlms")},min_strength:{type:"string",__default:"strong"},html_attrs:{__default:{minlength:8}}},fillEditAfter:function(e,t){var n=e.meter,r=e.meter_description;return n?Object(z.createElement)(z.Fragment,null,Object(z.createElement)("div",{className:"llms-pwd-meter"},Object(z.createElement)("div",null,Object($.__)("Very Weak","lifterlms"))),Object(z.createElement)(G.RichText,{style:{marginTop:0},tagName:"p",value:r,onChange:function(e){return t({meter_description:e})},allowedFormats:["core/bold","core/italic"],"aria-label":r?Object($.__)("Password strength meter description","lifterlms"):Object($.__)("Empty Password strength meter description; start writing to add a label"),placeholder:Object($.__)("Enter a description for the password strength meter","lifterlms")})):null},fillInspectorControls:function(e,t){var n=e.isConfirmationControlField,r=e.isConfirmationField,o=e.meter,i=e.min_strength,l=e.html_attrs,s=l.minlength;if(!r){return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Password strength meter","lifterlms"),help:o?Object($.__)("Password strength meter is enabled.","lifterlms"):Object($.__)("Password strength meter is disabled.","lifterlms"),checked:o,onChange:function(){return t({meter:!o})}}),o&&Object(z.createElement)(K.SelectControl,{label:Object($.__)("Minimum Password Strength","lifterlms"),value:i,onChange:function(e){return t({min_strength:e})},options:[{value:"strong",label:Object($.__)("Strong","lifterlms")},{value:"medium",label:Object($.__)("Medium","lifterlms")},{value:"weak",label:Object($.__)("Weak","lifterlms")}]}),Object(z.createElement)(K.TextControl,{label:Object($.__)("Minimum Password Length","lifterlms"),value:s,type:"number",min:"6",onChange:function(e){return function(e){t(kc(l,e)),n&&function(e){var t=Object(Xr.select)(G.store).getSelectedBlockClientId,n=Object(Xr.dispatch)(G.store).updateBlockAttributes,r=$s(t()),o=r.attributes;n(r.clientId,kc(o.html_attrs,e))}(e)}(1*e)}}))}}},["transforms","variations"]),Sc="llms/form-field-user-address",Pc=ra(),Ic=!0,Dc=oa(na("group"),{title:Object($.__)("User Address","lifterlms"),description:Object($.__)("A group of fields used to collect a user's full address.","lifterlms"),icon:{src:"id-alt"},supports:{inserter:!0,multiple:!1},llmsInnerBlocks:{allowed:["llms/form-field-user-address-street","llms/form-field-user-address-city","llms/form-field-user-address-country","llms/form-field-user-address-region"],template:[["llms/form-field-user-address-street"],["llms/form-field-user-address-city"],["llms/form-field-user-address-country"],["llms/form-field-user-address-region"]]}},["providesContext"]),Rc="llms/form-field-user-address-street",Tc=ra(),Mc=!0,Lc=oa(na("group"),{title:Object($.__)("User Street Address","lifterlms"),description:Object($.__)("Collect a user's street address.","lifterlms"),icon:{src:"id-alt"},supports:{multiple:!1},llmsInnerBlocks:{allowed:["llms/form-field-user-address-street-primary","llms/form-field-user-address-street-secondary"],template:[["llms/form-field-user-address-street-primary",{columns:8,last_column:!1}],["llms/form-field-user-address-street-secondary",{columns:4,last_column:!0}]]},parent:["llms/form-field-user-name"]}),Ac="llms/form-field-user-address-street-primary",Nc=!0,Fc=oa(Ka,{title:Object($.__)("User Street Address","lifterlms"),description:Object($.__)("A special field used to collect a user's street address.","lifterlms"),icon:{src:"admin-home"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,storage:!1}},attributes:{id:{__default:"llms_billing_address_1"},label:{__default:Object($.__)("Address","lifterlms")},name:{__default:"llms_billing_address_1"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_billing_address_1"}},parent:["llms/form-field-user-address-street"],usesContext:["llms/fieldGroup/fieldLayout"]},["transforms","variations"]),Bc="llms/form-field-user-address-street-secondary",Vc=!0,Uc=oa(Fc,{title:Object($.__)("User Street Address Additional Information","lifterlms"),description:Object($.__)("A special field used to collect a user's street address.","lifterlms"),attributes:{id:{__default:"llms_billing_address_2"},label:{__default:""},placeholder:{__default:Object($.__)("Apartment, suite, etc…","lifterlms")},name:{__default:"llms_billing_address_2"},required:{__default:!1},data_store_key:{__default:"llms_billing_address_2"},label_show_empty:{__default:!0}},usesContext:["llms/fieldGroup/fieldLayout"]},["transforms","variations"]),Hc="llms/form-field-user-address-city",qc=!0,zc=oa(Ka,{title:Object($.__)("User City","lifterlms"),description:Object($.__)("A special field used to collect a user's billing city.","lifterlms"),icon:{src:"location-alt"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,match:!1,storage:!1}},attributes:{id:{__default:"llms_billing_city"},label:{__default:Object($.__)("City","lifterlms")},name:{__default:"llms_billing_city"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_billing_city"}},parent:["llms/form-field-user-address"]},["transforms","variations"]),$c="llms/form-field-user-address-country",Wc=!0,Gc=oa(Fa,{title:Object($.__)("User Country","lifterlms"),description:Object($.__)("A special field used to collect a user's billing country.","lifterlms"),icon:{src:"admin-site"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,match:!1,storage:!1,options:!1}},attributes:{id:{__default:"llms_billing_country"},label:{__default:Object($.__)("Country","lifterlms")},name:{__default:"llms_billing_country"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_billing_country"},options_preset:{__default:"countries"},placeholder:{__default:Object($.__)("Select a Country","lifterlms")}},parent:["llms/form-field-user-address"]},["transforms"]),Kc="llms/form-field-user-address-region",Yc=ra(),Xc=!0,Qc=oa(na("group"),{title:Object($.__)("User Street Address","lifterlms"),description:Object($.__)("Collect a user's street address.","lifterlms"),icon:{src:"id-alt"},supports:{multiple:!1},parent:["llms/form-field-user-name"],llmsInnerBlocks:{allowed:["llms/form-field-user-address-state","llms/form-field-user-address-postal-code"],template:[["llms/form-field-user-address-state",{columns:6,last_column:!1}],["llms/form-field-user-address-postal-code",{columns:6,last_column:!0}]]}}),Jc="llms/form-field-user-address-state",Zc=!0,eu=oa(Fa,{title:Object($.__)("User Country","lifterlms"),description:Object($.__)("A special field used to collect a user's billing country.","lifterlms"),icon:{src:"location"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,storage:!1,options:!1}},attributes:{id:{__default:"llms_billing_state"},label:{__default:Object($.__)("State / Region","lifterlms")},name:{__default:"llms_billing_state"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_billing_state"},options_preset:{__default:"states"},placeholder:{__default:Object($.__)("Select a State / Region","lifterlms")}},parent:["llms/form-field-user-address-region"],usesContext:["llms/fieldGroup/fieldLayout"]},["transforms"]),tu="llms/form-field-user-address-postal-code",nu=!0,ru=oa(Ka,{title:Object($.__)("User Postal Code","lifterlms"),description:Object($.__)("A special field used to collect a user's postal or zip code.","lifterlms"),icon:{src:"post-status"},supports:{multiple:!1,llms_field_inspector:{id:!1,name:!1,required:!0,match:!1,storage:!1}},attributes:{id:{__default:"llms_billing_zip"},label:{__default:Object($.__)("Postal / Zip Code","lifterlms")},name:{__default:"llms_billing_zip"},required:{__default:!0},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_billing_zip"}},parent:["llms/form-field-user-address-region"],usesContext:["llms/fieldGroup/fieldLayout"]},["transforms","variations"]),ou="llms/form-field-user-phone",iu=!0,lu=oa(Ka,{title:Object($.__)("User Phone","lifterlms"),description:Object($.__)("A field used to collect a user's phone number.","lifterlms"),icon:{src:"phone"},supports:{inserter:!0,multiple:!1,llms_field_inspector:{id:!1,name:!1,storage:!1}},attributes:{id:{__default:"llms_phone"},field:{__default:"tel"},label:{__default:Object($.__)("Phone Number","lifterlms")},name:{__default:"llms_phone"},data_store:{__default:"usermeta"},data_store_key:{__default:"llms_phone"}}},["transforms","variations"]),su=function(){var e=Object(H.applyFilters)("llms.formBlocksSafelist",["core/block","core/paragraph","core/heading","core/image","core/html","core/column","core/columns","core/group","core/separator","core/spacer"]),t=(0,Object(Xr.select)(Qr.store).getCurrentPost)().meta,n=(void 0===t?{}:t)._llms_form_location;Object(Jr.getBlockTypes)().forEach((function(t){var r=t.name;(function(t){return-1===e.indexOf(t)&&(0===t.indexOf("llms/form-field-redeem-voucher")?"registration"!==n:0===t.indexOf("llms/form-field-user-login")?"account"===n:-1===t.indexOf("llms/form-field"))})(r)&&Object(Jr.unregisterBlockType)(r)}))};Yr()((function(){var e=Object(Xr.select)(Qr.store).getCurrentPost,t=!1,n=Object(Xr.subscribe)((function(){var r=e();if(!1===t&&0!==Object.keys(r).length){t=!0,n();var o=r.type,i=r.is_llms_field;"llms_form"===o?(to(),su(),Bo()):"wp_block"===o&&"yes"===i&&(su(),Bo())}}))}));var au=n(27),cu=n(21),uu=Object(H.applyFilters)("llms_blocks_post_visibility_options",[{value:"catalog_search",label:Object($.__)("Visible","lifterlms"),info:Object($.__)("Visible in the catalog and search results.","lifterlms")},{value:"catalog",label:Object($.__)("Catalog only","lifterlms"),info:Object($.__)("Only visible in the catalog.","lifterlms")},{value:"search",label:Object($.__)("Search only","lifterlms"),info:Object($.__)("Only visible in search results.","lifterlms")},{value:"hidden",label:Object($.__)("Hidden","lifterlms"),info:Object($.__)("Hidden from catalog and search results.","lifterlms")}]),fu=Object(Xr.withSelect)((function(e){return{visibility:e("core/editor").getEditedPostAttribute("visibility")}}))((function(e){var t=e.visibility;return uu.find((function(e){return e.value===t})).label}));var du=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){return X()(this,o),r.apply(this,arguments)}return J()(o,[{key:"render",value:function(){var e=this.props,t=e.onUpdateVisibility,n=e.instanceId,r=e.visibility,o={catalog_search:{checked:"catalog_search"===r},catalog:{checked:"catalog"===r},search:{checked:"search"===r},hidden:{checked:"hidden"===r}};return Object(z.createElement)(cu.PluginPostStatusInfo,{className:"llms-post-visibility"},Object(z.createElement)("span",null,Object($.__)("Catalog & Search Visibility","lifterlms")),Object(z.createElement)("div",null,Object(z.createElement)(K.Dropdown,{className:"llms-post-visibility-dropdown",contentClassName:"llms-post-visibility-content edit-post-post-visibility__dialog",renderToggle:function(e){var t=e.isOpen,n=e.onToggle;return Object(z.createElement)(K.Button,{onClick:n,"aria-expanded":t,isLink:!0},Object(z.createElement)(fu,null))},renderContent:function(){return Object(z.createElement)("fieldset",{key:"visibility-selector",className:"editor-post-visibility__dialog-fieldset"},Object(z.createElement)("legend",{className:"editor-post-visibility__dialog-legend"},Object($.__)("Catalog Visibility","lifterlms")),uu.map((function(e){var r=e.value,i=e.label,l=e.info;return Object(z.createElement)("div",{key:r,className:"editor-post-visibility__choice"},Object(z.createElement)("input",{type:"radio",name:"llms-editor-post-visibility__setting-".concat(n),value:r,onChange:function(){return t(r)},checked:o[r].checked,id:"editor-post-".concat(r,"-").concat(n),"aria-describedby":"editor-post-".concat(r,"-").concat(n,"-description"),className:"editor-post-visibility__dialog-radio"}),Object(z.createElement)("label",{htmlFor:"editor-post-".concat(r,"-").concat(n),className:"editor-post-visibility__dialog-label"},i),Object(z.createElement)("p",{id:"llms-editor-post-".concat(r,"-").concat(n,"-description"),className:"editor-post-visibility__dialog-info"},l))})))}})))}}]),o}(z.Component),pu=Object(W.compose)([Object(Xr.withSelect)((function(e){var t=e("core/editor"),n=t.getCurrentPostType,r=t.getEditedPostAttribute;return{postType:n(),visibility:r("visibility")}})),Object(Xr.withDispatch)((function(e){var t=e("core/editor").editPost;return{onUpdateVisibility:function(e){t({visibility:e})}}})),Object(W.ifCondition)((function(e){var t=e.postType;return-1!==["course","llms_membership"].indexOf(t)})),W.withInstanceId])(du);Object(au.registerPlugin)("llms-post-visibility",{render:pu});var mu=wp.element.Fragment,bu={fillRule:"evenodd",clipRule:"evenodd",strokeLinejoin:"round",strokeMiterlimit:1.41421},hu=function(){return Object(z.createElement)(mu,null,Object(z.createElement)("svg",{width:"20px",height:"20px",viewBox:"0 0 85 85",version:"1.1",style:bu},Object(z.createElement)("g",{id:"lifterlms-icon"},Object(z.createElement)("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"}),Object(z.createElement)("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"}),Object(z.createElement)("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"}))))},vu=(n(73),n(23));function gu(e){var t=e.text,n=e.onSuccess,r=void 0!==W.useCopyToClipboard;return Object(z.createElement)(K.Tooltip,{text:Object($.__)("Click to copy.","lifterlms")},r&&Object(z.createElement)((function(){var e=Object(W.useCopyToClipboard)(t,n);return Object(z.createElement)(K.Button,{isLink:!0,ref:e},t)}),null),!r&&Object(z.createElement)((function(){return Object(z.createElement)(K.ClipboardButton,{isLink:!0,text:t,onCopy:n},t)}),null))}var yu=function(e){var t=e.closeModal,n=e.isActive,r=e.onChange,o=e.searchQuery,i=e.value,l=e.defaultValue,s=window.llms.userInfoFields;o&&(s=s.filter((function(e){return function(e,t){var n=[t.label,t.name,t.id,t.data_store_key],r=e.toLowerCase();return n.some((function(e){return e.toLowerCase().includes(r)}))}(o,e)})));var a=!s.length;a&&s.push({data_store_key:o,label:Object($.__)("Custom User Information","lifterlms"),id:"custom",name:o});var c=Object(H.applyFilters)("llms/userInfoShortcodes/exclude",["password"]);return s=s.filter((function(e){var t=e.id;return!c.includes(t)})),Object(z.createElement)(z.Fragment,null,a&&Object(z.createElement)("p",{className:"llms-error"},Object($.__)("No fields found matching your search but you can use the shortcode below if the meta information exists in the database.","lifterlms")),Object(z.createElement)("table",{className:"llms-table zebra"},Object(z.createElement)("thead",null,Object(z.createElement)("tr",null,Object(z.createElement)("th",null,Object($.__)("Name","lifterlms")),Object(z.createElement)("th",null,Object($.__)("Shortcode","lifterlms")),Object(z.createElement)("th",null,Object($.__)("Insert","lifterlms")))),Object(z.createElement)("tbody",null,s.map((function(e){return function(e,t,n,r,o,i){var l=e.label,s=e.name,a=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=t?' or="'.concat(t,'"'):"";return"[llms-user ".concat(e).concat(n,"]")}(e.data_store_key,i);return Object(z.createElement)("tr",{key:s},Object(z.createElement)("td",null,l),Object(z.createElement)("td",null,Object(z.createElement)(gu,{text:a,onSuccess:n})),Object(z.createElement)("td",null,Object(z.createElement)(K.Button,{isSecondary:!0,isSmall:!0,onClick:function(){var e=Object(vu.create)({html:'<span class="llms-user-sc-wrap">'.concat(a,"</span>")});n(),r(t?Object(vu.replace)(o,/\[user .+?\]/,e):Object(vu.insert)(o,e))}},Object($.__)("Insert","lifterlms"))))}(e,n,t,r,i,l)})))))};Object(vu.registerFormatType)("llms/user-info-shortcodes",{title:Object($.__)("LifterLMS User Information Shortcodes","lifterlms"),tagName:"span",className:"llms-user-sc-wrap",edit:function(e){var t=Object(z.useState)(!1),n=Di()(t,2),r=n[0],o=n[1],i=Object(z.useState)(""),l=Di()(i,2),s=l[0],a=l[1],c=Object(z.useState)(""),u=Di()(c,2),f=u[0],d=u[1],p=function(){return o(!1)},m=e.value,b=e.onChange,h=e.isActive;return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(G.RichTextToolbarButton,{icon:Object(z.createElement)(hu,null),title:Object($.__)("Shortcodes","lifterlms"),onClick:function(){return o(!0)}}),r&&Object(z.createElement)(K.Modal,{className:"llms-shortcodes-modal",title:Object($.__)("LifterLMS User Information Shortcodes","lifterlms"),onRequestClose:p},Object(z.createElement)("div",{className:"llms-shortcodes-modal--main"},Object(z.createElement)("aside",null,Object(z.createElement)(K.TextControl,{type:"search",label:Object($.__)("Filter by label, key, or ID…","lifterlms"),onChange:function(e){return a(e)}}),Object(z.createElement)(K.TextControl,{label:Object($.__)("Default value","lifterlms"),onChange:function(e){return d(e)},help:Object($.__)("Optional text displayed when no information exists or the user is logged out.","lifterlms")})),Object(z.createElement)("section",null,Object(z.createElement)(yu,{closeModal:p,isActive:h,onChange:b,searchQuery:s,value:m,defaultValue:f})))))}});var Ou=n(38),_u=function(e){var t=e.id,n=e.item,r=e.index,o=e.setNodeRef,i=e.listeners,l=e.manageState,s=n.visibility,a=n.name,c=n.label,u=l.updateItem,f=l.deleteItem,d="visible"===s,p=0===r,m=Object(z.useState)(!1),b=Di()(m,2),h=b[0],v=b[1];return Object(z.createElement)(z.Fragment,null,Object(z.createElement)("div",{className:"llms-instructor--header"},Object(z.createElement)("section",null,Object(z.createElement)("strong",null,a),Object(z.createElement)("small",null,"(#",t,")")),Object(z.createElement)("aside",null,p&&Object(z.createElement)(K.Tooltip,{text:Object($.__)("Primary Instructor","lifterlms")},Object(z.createElement)(K.Dashicon,{icon:"star-filled"})),Object(z.createElement)(Ls,{label:Object($.__)("Reorder instructor","lifterlms"),setNodeRef:o,listeners:i}),Object(z.createElement)(K.Button,{isSmall:!0,showTooltip:!0,label:Object($.__)("Edit instructor","lifterlms"),icon:h?"arrow-up-alt2":"arrow-down-alt2",onClick:function(){return v(!h)}}))),h&&Object(z.createElement)("div",{className:"llms-instructor--settings"},Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Visibility","lifterlms"),help:d?Object($.__)("Instructor is visible on frontend","lifterlms"):Object($.__)("Instructor is hidden on frontend","lifterlms"),checked:d,onChange:function(e){return u(t,{visibility:e?"visible":"hidden"})}}),d&&Object(z.createElement)(K.TextControl,{label:Object($.__)("Label","lifterlms"),value:c,onChange:function(e){return u(t,{label:e})}}),Object(z.createElement)(K.Button,{isSecondary:!0,iconPosition:"right",href:Object(Ou.addQueryArgs)("/wp-admin/user-edit.php",{user_id:t}),target:"_blank",rel:"noreferrer",style:{marginRight:"5px"}},Object($.__)("Edit","lifterlms"),Object(z.createElement)(K.Dashicon,{icon:"external"})),!p&&Object(z.createElement)(K.Button,{isDestructive:!0,onClick:function(){return f(n)}},Object($.__)("Remove","lifterlms"))))},ju=function(e){var t=e.instructors,n=e.roles,r=e.addInstructor;return Object(z.createElement)(Ui,{roles:n,placeholder:Object($.__)("Search…","lifterlms"),searchArgs:{exclude:t.map((function(e){return e.id}))},onChange:r})};function wu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function xu(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?wu(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):wu(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}n(74);var Eu=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o),e=r.apply(this,arguments),U()(ce()(e),"onSearchChange",(function(t){e.setState({search:t})})),U()(ce()(e),"updateInstructor",(function(t,n){var r=e.state.instructors.map((function(e){return t===e.id&&(e=xu(xu({},e),n)),e}));e.updateInstructors(r)})),U()(ce()(e),"updateInstructors",(function(t){e.setState({instructors:t}),e.props.updateInstructors(t)})),U()(ce()(e),"addInstructor",(function(t){var n=t.id,r=t.name,o=e.state.instructors;o.push(xu(xu({},e.getInstructorDefaults()),{},{id:n,name:r})),e.updateInstructors(o)})),U()(ce()(e),"removeInstructor",(function(t){var n=e.state.instructors;n=n.filter((function(e){var n=e.id;return t.id!==n})),e.updateInstructors(n)})),U()(ce()(e),"render",(function(){return Object(z.createElement)(K.PanelBody,{title:Object($.__)("Instructors","lifterlms")},Object(z.createElement)(ju,{roles:e.getRoles(),instructors:e.state.instructors,addInstructor:e.addInstructor}),Object(z.createElement)(Ns,{ListItem:_u,items:e.state.instructors,itemClassName:"llms-instructor",manageState:{createItem:e.addInstructor,deleteItem:e.removeInstructor,updateItem:e.updateInstructor,updateItems:e.updateInstructors}}))}));var t=e.props.instructors;return t="string"==typeof t?JSON.parse(t):t,e.state={instructors:t||[],search:""},e}return J()(o,[{key:"getRoles",value:function(){return Object(H.applyFilters)("llms_instructor_roles",["administrator","lms_manager","instructor","instructors_assistant"])}},{key:"getInstructorDefaults",value:function(){return Object(H.applyFilters)("llms_instructor_defaults",{label:Object($.__)("Author","lifterlms"),visibility:"visible"})}}]),o}(z.Component),ku=Object(Xr.withSelect)((function(e){return{instructors:(0,e("core/editor").getEditedPostAttribute)("instructors")}})),Cu=Object(Xr.withDispatch)((function(e){var t=e("core/editor").editPost;return{updateInstructors:function(e){t({instructors:JSON.stringify(e)})}}})),Su=Object(W.compose)([ku,Cu])(Eu),Pu=n(39),Iu=Object(K.createSlotFill)("LLMSFormDocSettings"),Du=Iu.Fill,Ru=Iu.Slot,Tu=function(e){var t=e.children;return Object(z.createElement)(Du,null,t)};Tu.Slot=Ru,window.llms.plugins=window.llms.plugins||{},window.llms.plugins.LLMSFormDocSettings=Tu;var Mu=Tu;var Lu,Au,Nu=function(e){ee()(o,e);var t,n,r=(t=o,n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,r=oe()(t);if(n){var o=oe()(this).constructor;e=Reflect.construct(r,arguments,o)}else e=r.apply(this,arguments);return ne()(this,e)});function o(){var e;X()(this,o);for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return e=r.call.apply(r,[this].concat(n)),U()(ce()(e),"render",(function(){if(void 0===cu.PluginDocumentSettingPanel)return null;if("llms_form"!==Object(Xr.select)(Qr.store).getCurrentPostType())return null;var t=e.props,n=t.location,r=t.link,o=t.showTitle,i=t.freeApTitle,l=t.setFormMetas,s=window.llms.formLocations[n];function a(e){Object(Xr.dispatch)(G.store).replaceBlocks(Object(Xr.select)(G.store).getBlocks().map((function(e){return e.clientId})),Object(Jr.parse)(e))}function c(){var e="llms-form-restore-default",t=Object(Xr.select)(Qr.store).getEditedPostAttribute("content"),n=Object(Xr.dispatch)(Pu.store),r=n.createSuccessNotice,o=n.removeNotice,i=Object(Xr.dispatch)(Lo).resetFields;i(),a(s.template),r(Object($.__)("The form has been restored to the default template.","lifterlms"),{id:e,actions:[{label:Object($.__)("Undo","lifterlms"),onClick:function(){i(),a(t),o(e)}}]})}return""===o&&l({_llms_form_show_title:"yes"}),Object(z.createElement)(z.Fragment,null,Object(z.createElement)(cu.PluginDocumentSettingPanel,{className:"llms-forms-doc-settings",name:"llms-forms-doc-settings",title:Object($.__)("Form Settings","lifterlms"),opened:!0},Object(z.createElement)(Mu.Slot,null,(function(e){return Object(z.createElement)(z.Fragment,null,Object(z.createElement)(K.PanelRow,null,Object(z.createElement)("strong",null,Object($.__)("Location","lifterlms")),!r&&Object(z.createElement)("strong",null,s.name),r&&Object(z.createElement)(K.ExternalLink,{href:r},s.name)),Object(z.createElement)("p",{style:{marginTop:"5px"}},Object(z.createElement)("em",null,s.description)),e,Object(z.createElement)("br",null),Object(z.createElement)(K.ToggleControl,{label:Object($.__)("Display Form Title","lifterlms"),checked:"yes"===o,help:"yes"===o?Object($.__)("Displaying form title.","lifterlms"):Object($.__)("Not displaying form title.","lifterlms"),onChange:function(e){return l({_llms_form_show_title:e?"yes":"no"})}}),"checkout"===n&&"yes"===o&&Object(z.createElement)(K.TextControl,{label:Object($.__)("Free Access Plan Form Title","lifterlms"),value:i,onChange:function(e){return l({_llms_form_title_free_access_plans:e})},help:Object($.__)("The form title to be shown for free access plans.","lifterlms")}),Object(z.createElement)("br",null),Object(z.createElement)(K.PanelRow,null,Object(z.createElement)(K.Button,{isDestructive:!0,onClick:c},Object($.__)("Revert to Default","lifterlms"))),Object(z.createElement)("p",{style:{marginTop:"5px"}},Object(z.createElement)("em",null,Object($.__)("Replace the existing content of the form with the original default content.","lifterlms"))))}))))})),e}return o}(z.Component),Fu=Object(Xr.withSelect)((function(e){var t=e(Qr.store),n=t.getCurrentPost,r=t.getCurrentPostType,o=t.getEditedPostAttribute;if("llms_form"!==r())return{};var i=o("meta");return{link:n().link,location:i._llms_form_location,showTitle:i._llms_form_show_title,freeApTitle:i._llms_form_title_free_access_plans}})),Bu=Object(Xr.withDispatch)((function(e){var t=e("core/editor").editPost;return{setFormMetas:function(e){t({meta:e})}}})),Vu=Object(W.compose)([Fu,Bu])(Nu);function Uu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}Object(au.registerPlugin)("llms",{render:function(){return-1!==["course","llms_membership"].indexOf(Object(Xr.select)("core/editor").getCurrentPostType())?Object(z.createElement)(z.Fragment,null,Object(z.createElement)(cu.PluginSidebarMoreMenuItem,{target:"llms-sidebar",icon:Object(z.createElement)(hu,null)},"LifterLMS"),Object(z.createElement)(cu.PluginSidebar,{name:"llms-sidebar",title:"LifterLMS"},Object(z.createElement)(Su,null))):null},icon:Object(z.createElement)(hu,null)}),Object(au.registerPlugin)("llms-forms-doc-settings",{render:Vu,icon:""}),Lu=eo(),Au=[i,l,s,a,c,u,f,d,p],Object.keys(B).forEach((function(e){B[e].composed&&Au.push(B[e])})),["llms_form","wp_block"].includes(Lu)&&Object(H.doAction)("llms_form_fields_ready",B),Au.forEach((function(e){var t=e.name,n=e.postTypes,r=e.settings;n&&-1===n.indexOf(Lu)||Object(Jr.registerBlockType)(t,r)})),window.llms.components=function(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?Uu(Object(n),!0).forEach((function(t){U()(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Uu(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}({},m)}]); \ No newline at end of file diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-information-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-information-block.php new file mode 100644 index 0000000000..f1407e9e09 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-information-block.php @@ -0,0 +1,169 @@ +<?php +/** + * Course information block. + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 1.1.0 + * + * @render_hook llms_course-information-block_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course information block class. + */ +class LLMS_Blocks_Course_Information_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'course-information'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + * @since 1.0.0 + * @version 1.1.0 + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + $attributes = wp_parse_args( + $attributes, + array( + 'title' => __( 'Course Information', 'lifterlms' ), + 'title_size' => 'h3', + 'show_length' => true, + 'show_difficulty' => true, + 'show_tracks' => true, + 'show_cats' => true, + 'show_tags' => true, + ) + ); + + $show_wrappers = false; + + if ( $attributes['show_length'] ) { + $show_wrappers = true; + add_action( $this->get_render_hook(), 'lifterlms_template_single_length', 10 ); + } + + if ( $attributes['show_difficulty'] ) { + $show_wrappers = true; + add_action( $this->get_render_hook(), 'lifterlms_template_single_difficulty', 20 ); + } + + if ( $attributes['show_tracks'] ) { + $show_wrappers = true; + add_action( $this->get_render_hook(), 'lifterlms_template_single_course_tracks', 25 ); + } + + if ( $attributes['show_cats'] ) { + $show_wrappers = true; + add_action( $this->get_render_hook(), 'lifterlms_template_single_course_categories', 30 ); + } + + if ( $attributes['show_tags'] ) { + $show_wrappers = true; + add_action( $this->get_render_hook(), 'lifterlms_template_single_course_tags', 35 ); + } + + if ( $show_wrappers ) { + + $this->title = $attributes['title']; + $this->title_size = $attributes['title_size']; + + add_filter( 'llms_course_meta_info_title', array( $this, 'filter_title' ) ); + add_filter( 'llms_course_meta_info_title_size', array( $this, 'filter_title_size' ) ); + + add_action( $this->get_render_hook(), 'lifterlms_template_single_meta_wrapper_start', 5 ); + add_action( $this->get_render_hook(), 'lifterlms_template_single_meta_wrapper_end', 50 ); + + } + + } + + /** + * Filters the title of the course information headline per block settings. + * + * @param string $title default title. + * @return string + * @since 1.0.0 + * @version 1.0.0 + */ + public function filter_title( $title ) { + return $this->title; + } + + /** + * Filters the title headline element size of the course information headline per block settings. + * + * @param string $size default size. + * @return string + * @since 1.0.0 + * @version 1.0.0 + */ + public function filter_title_size( $size ) { + return $this->title_size; + } + + /** + * Register meta attributes stub. + * + * Called after registering the block type. + * + * @return void + * @since 1.0.0 + * @version 1.0.0 + */ + public function register_meta() { + + register_meta( + 'post', + '_llms_length', + array( + 'object_subtype' => 'course', + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => array( $this, 'meta_auth_callback' ), + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ) + ); + + } + + /** + * Meta field update authorization callback. + * + * @param bool $allowed Is the update allowed. + * @param string $meta_key Meta keyname. + * @param int $object_id WP Object ID (post,comment,etc)... + * @param int $user_id WP User ID. + * @param string $cap requested capability. + * @param array $caps user capabilities. + * @return bool + * @since 1.0.0 + * @version 1.0.0 + */ + public function meta_auth_callback( $allowed, $meta_key, $object_id, $user_id, $cap, $caps ) { + return true; + } + +} + +return new LLMS_Blocks_Course_Information_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-progress-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-progress-block.php new file mode 100644 index 0000000000..31541749b5 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-progress-block.php @@ -0,0 +1,92 @@ +<?php +/** + * Course progress bar block + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.9.0 + * @version 1.9.0 + * + * @render_hook llms_course-progress_block_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course progress block class. + */ +class LLMS_Blocks_Course_Progress_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'course-progress'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @since 1.9.0 + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), array( $this, 'output' ), 10 ); + + } + + /** + * Output the course progress bar + * + * @since 1.9.0 + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @return void + */ + public function output( $attributes = array() ) { + + $block_content = ''; + $progress = do_shortcode( '[lifterlms_course_progress check_enrollment=1]' ); + $class = empty( $attributes['className'] ) ? '' : $attributes['className']; + + if ( $progress ) { + $block_content = sprintf( + '<div class="wp-block-%1$s-%2$s%3$s">%4$s</div>', + $this->vendor, + $this->id, + // Take into account the custom class attribute. + empty( $attributes['className'] ) ? '' : ' ' . esc_attr( $attributes['className'] ), + $progress + ); + } + + /** + * Filters the block html + * + * @since 1.9.0 + * + * @param string $block_content The block's html. + * @param array $attributes The block's array of attributes. + * @param LLMS_Blocks_Course_Progress_Block $block This block object. + */ + $block_content = apply_filters( 'llms_blocks_render_course_progress_block', $block_content, $attributes, $this ); + + if ( $block_content ) { + echo $block_content; + } + + } +} + +return new LLMS_Blocks_Course_Progress_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-syllabus-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-syllabus-block.php new file mode 100644 index 0000000000..1de10bd48c --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-course-syllabus-block.php @@ -0,0 +1,71 @@ +<?php +/** + * Course syllabus block. + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 1.1.0 + * + * @render_hook llms_course-syllabus-block_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course syllabus block class. + */ +class LLMS_Blocks_Course_Syllabus_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'course-syllabus'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + * @since 1.0.0 + * @version 1.1.0 + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), 'lifterlms_template_single_syllabus', 10 ); + + } + + /** + * Retrieve custom block attributes. + * Necessary to override when creating ServerSideRender blocks. + * + * @return array + * @since 1.0.0 + * @version 1.0.0 + */ + public function get_attributes() { + return array_merge( + parent::get_attributes(), + array( + 'course_id' => array( + 'type' => 'int', + 'default' => 0, + ), + ) + ); + } + +} + +return new LLMS_Blocks_Course_Syllabus_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-instructors-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-instructors-block.php new file mode 100644 index 0000000000..04aed6ec84 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-instructors-block.php @@ -0,0 +1,101 @@ +<?php +/** + * Instructors block. + * + * Render hook: llms_instructors-block_render + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 1.11.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course syllabus block class. + * + * @since 1.0.0 + */ +class LLMS_Blocks_Instructors_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'instructors'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @since 1.0.0 + * @since 1.1.0 Unknown. + * @since 1.11.1 Add support for memberships. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + switch ( get_post_type() ) { + case 'course': + $func = 'lifterlms_template_course_author'; + break; + + case 'llms_membership': + $func = 'llms_template_membership_instructors'; + break; + + default: + return; + } + + add_action( $this->get_render_hook(), $func, 10 ); + + } + + /** + * Retrieve custom block attributes. + * + * Necessary to override when creating ServerSideRender blocks. + * + * @since 1.0.0 + * + * @return array + */ + public function get_attributes() { + return array_merge( + parent::get_attributes(), + array( + 'post_id' => array( + 'type' => 'int', + 'default' => 0, + ), + ) + ); + } + + /** + * Output a message when no HTML was rendered + * + * @since 1.0.0 + * @since 1.8.0 Fixed spelling error. + * + * @return string + */ + public function get_empty_render_message() { + return __( 'No visible instructors were found.', 'lifterlms' ); + } + +} + +return new LLMS_Blocks_Instructors_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-navigation-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-navigation-block.php new file mode 100644 index 0000000000..3a8b287593 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-navigation-block.php @@ -0,0 +1,71 @@ +<?php +/** + * Lesson Navigation block. + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 1.1.0 + * + * @render_hook llms_lesson-navigation-block_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course syllabus block class. + */ +class LLMS_Blocks_Lesson_Navigation_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'lesson-navigation'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + * @since 1.0.0 + * @version 1.1.0 + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), 'lifterlms_template_lesson_navigation', 10 ); + + } + + /** + * Retrieve custom block attributes. + * Necessary to override when creating ServerSideRender blocks. + * + * @return array + * @since 1.0.0 + * @version 1.0.0 + */ + public function get_attributes() { + return array_merge( + parent::get_attributes(), + array( + 'post_id' => array( + 'type' => 'int', + 'default' => 0, + ), + ) + ); + } + +} + +return new LLMS_Blocks_Lesson_Navigation_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-progression-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-progression-block.php new file mode 100644 index 0000000000..de07ea34d6 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-lesson-progression-block.php @@ -0,0 +1,120 @@ +<?php +/** + * Lesson Progression block. + * + * Render hook: llms_lesson-progression-block_render + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 2.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Lesson progression block + * + * @since 1.0.0 + * @since 1.1.0 Unknown + * @since 1.7.0 Don't output an empty render message for free lessons. + * @since 1.8.0 Register meta data used by the block editor. + */ +class LLMS_Blocks_Lesson_Progression_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'lesson-progression'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @since 1.0.0 + * @since 1.1.0 Unknown. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), 'lifterlms_template_complete_lesson_link', 10 ); + + } + + /** + * Output a message when no HTML was rendered + * + * @since 1.7.0 + * @since 2.0.0 Ensure the queried object is an `LLMS_Lesson` before checking if it's free. + * + * @return string + */ + public function get_empty_render_message() { + $lesson = llms_get_post( get_the_ID() ); + if ( $lesson && is_a( $lesson, 'LLMS_Lesson' ) && $lesson->is_free() ) { + return ''; + } + return parent::get_empty_render_message(); + } + + /** + * Retrieve custom block attributes. + * + * Necessary to override when creating ServerSideRender blocks. + * + * @since 1.0.0 + * + * @return array + */ + public function get_attributes() { + return array_merge( + parent::get_attributes(), + array( + 'post_id' => array( + 'type' => 'int', + 'default' => 0, + ), + ) + ); + } + + /** + * Register meta attributes. + * + * Called after registering the block type. + * + * @since 1.0.0 + * + * @return void + */ + public function register_meta() { + + register_meta( + 'post', + '_llms_quiz', + array( + 'object_subtype' => 'lesson', + 'sanitize_callback' => 'absint', + 'auth_callback' => '__return_true', + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ) + ); + + } + +} + +return new LLMS_Blocks_Lesson_Progression_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-php-template-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-php-template-block.php new file mode 100644 index 0000000000..901c38c3ef --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-php-template-block.php @@ -0,0 +1,144 @@ +<?php +/** + * PHP Template block class file. + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 2.3.0 + * + * @render_hook llms_php-template_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * PHP Template block. + * + * @since 2.3.0 + */ +class LLMS_Blocks_PHP_Template_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'php-template'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Templates map, where the keys are the template attribute value and the values are the php file names (w/o extension). + * + * @var array + */ + protected $templates = array( + 'archive-course' => 'loop-main', + 'archive-llms_membership' => 'loop-main', + 'taxonomy-course_cat' => 'loop-main', + 'taxonomy-course_difficulty' => 'loop-main', + 'taxonomy-course_tag' => 'loop-main', + 'taxonomy-course_track' => 'loop-main', + 'taxonomy-membership_cat' => 'loop-main', + 'taxonomy-membership_tag' => 'loop-main', + 'single-certificate' => 'content-certificate', + 'single-no-access' => 'content-no-access', + ); + + /** + * Add actions attached to the render function action. + * + * @since 2.3.0 + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), array( $this, 'output' ), 10 ); + + } + + /** + * Retrieve custom block attributes. + * + * Necessary to override when creating ServerSideRender blocks. + * + * @since 2.3.0 + * + * @return array + */ + public function get_attributes() { + return array( + 'template' => array( + 'type' => 'string', + 'default' => '', + ), + 'title' => array( + 'type' => 'string', + 'default' => '', + ), + ); + } + + /** + * Output the template. + * + * @since 2.3.0 + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @return void + */ + public function output( $attributes = array() ) { + + if ( empty( $attributes['template'] ) ) { + return; + } + + /** + * Filters the php templates that can be render via this block. + * + * @since 2.3.0 + * + * @param array $templates Templates map, where the keys are the template attribute value and the values are the php file names (w/o extension). + */ + $templates = apply_filters( 'llms_blocks_php_templates_block', $this->templates ); + + if ( ! array_key_exists( $attributes['template'], $templates ) ) { + return; + } + + ob_start(); + + llms_get_template( "{$templates[$attributes['template']]}.php" ); + + $block_content = ob_get_clean(); + + /** + * Filters the block html. + * + * @since 2.3.0 + * + * @param string $block_content The block's html. + * @param array $attributes The block's array of attributes. + * @param array $template The template file basename to be rendered. + * @param LLMS_Blocks_PHP_Template_Block $block This block object. + */ + $block_content = apply_filters( 'llms_blocks_render_php_template_block', $block_content, $attributes, $templates[ $attributes['template'] ], $this ); + + if ( $block_content ) { + echo $block_content; + } + + } + +} + +return new LLMS_Blocks_PHP_Template_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-pricing-table-block.php b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-pricing-table-block.php new file mode 100644 index 0000000000..a47d848c10 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/class-llms-blocks-pricing-table-block.php @@ -0,0 +1,129 @@ +<?php +/** + * Course pricing table block + * + * @package LifterLMS_Blocks/Blocks + * + * @since 1.0.0 + * @version 1.9.0 + * + * @render_hook llms_pricing-table-block_render + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Course syllabus block class + * + * @since 1.0.0 + * @since 1.3.7 Unknown. + * @since 1.9.0 Added `llms_blocks_render_pricing_table_block` filter. + */ +class LLMS_Blocks_Pricing_Table_Block extends LLMS_Blocks_Abstract_Block { + + /** + * Block ID. + * + * @var string + */ + protected $id = 'pricing-table'; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = true; + + /** + * Add actions attached to the render function action. + * + * @since 1.0.0 + * @since 1.1.0 Unknown. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + */ + public function add_hooks( $attributes = array(), $content = '' ) { + + add_action( $this->get_render_hook(), array( $this, 'output' ), 10 ); + + } + + /** + * Retrieve custom block attributes + * + * Necessary to override when creating ServerSideRender blocks. + * + * @since 1.0.0 + * @since 1.3.6 Unknown. + * + * @return array + */ + public function get_attributes() { + return array_merge( + parent::get_attributes(), + array( + 'post_id' => array( + 'type' => 'int', + 'default' => 0, + ), + ) + ); + } + + /** + * Output the pricing table. + * + * @since 1.0.0 + * @since 1.3.7 Unknown. + * @since 1.9.0 Added `llms_blocks_render_pricing_table_block` filter. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @return void + */ + public function output( $attributes = array() ) { + + ob_start(); + if ( 'edit' === filter_input( INPUT_GET, 'context' ) ) { + $id = filter_input( INPUT_GET, 'post_id', FILTER_SANITIZE_NUMBER_INT ); + if ( $id ) { + $product = new LLMS_Product( $id ); + if ( ! $product->get_access_plans() ) { + echo '<p>' . __( 'No access plans found.', 'lifterlms' ) . '</p>'; + } + } + + // force display of the table on the admin panel. + add_filter( 'llms_product_pricing_table_enrollment_status', '__return_false' ); + add_filter( 'llms_product_is_purchasable', '__return_true' ); + + } + + lifterlms_template_pricing_table( $attributes['post_id'] ); + + $block_content = ob_get_clean(); + + /** + * Filters the block html + * + * @since 1.9.0 + * + * @param string $block_content The block's html. + * @param array $attributes The block's array of attributes. + * @param LLMS_Blocks_Pricing_Table_Block $block This block object. + */ + $block_content = apply_filters( 'llms_blocks_render_pricing_table_block', $block_content, $attributes, $this ); + + remove_filter( 'llms_product_pricing_table_enrollment_status', '__return_false' ); + remove_filter( 'llms_product_is_purchasable', '__return_true' ); + + if ( $block_content ) { + echo $block_content; + } + + } +} + +return new LLMS_Blocks_Pricing_Table_Block(); diff --git a/libraries/lifterlms-blocks/includes/blocks/index.php b/libraries/lifterlms-blocks/includes/blocks/index.php new file mode 100644 index 0000000000..82e2315c6b --- /dev/null +++ b/libraries/lifterlms-blocks/includes/blocks/index.php @@ -0,0 +1,2 @@ +<?php +// silence. diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-abstract-block.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-abstract-block.php new file mode 100644 index 0000000000..ea35c617f2 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-abstract-block.php @@ -0,0 +1,175 @@ +<?php +/** + * Common block registration methods. + * + * @package LifterLMS_Blocks/Abstracts + * + * @since 1.0.0 + * @version 1.8.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Abstract Block class. + * + * @since 1.0.0 + * @since 1.8.0 Don't output empty render messages on the frontend. + */ +abstract class LLMS_Blocks_Abstract_Block { + + /** + * Block vendor ID. + * + * @var string + */ + protected $vendor = 'llms'; + + /** + * Block ID. + * + * @var string + */ + protected $id = ''; + + /** + * Is block dynamic (rendered in PHP). + * + * @var bool + */ + protected $is_dynamic = false; + + /** + * Constructor. + * + * @since 1.0.0 + * @version 1.0.0 + */ + public function __construct() { + + if ( $this->is_dynamic ) { + + register_block_type( + $this->get_block_id(), + array( + 'attributes' => $this->get_attributes(), + 'render_callback' => array( $this, 'render_callback' ), + ) + ); + + } + + $this->register_meta(); + + } + + /** + * Add hooks stub. + * Extending classes can use this class to add hooks attached to the render function action. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return void + * @since 1.0.0 + * @version 1.0.0 + */ + public function add_hooks( $attributes = array(), $content = '' ) {} + + /** + * Retrieve custom block attributes. + * Necessary to override when creating ServerSideRender blocks. + * + * @return array + * @since 1.0.0 + * @version 1.0.0 + */ + public function get_attributes() { + return LLMS_Blocks_Visibility::get_attributes(); + } + + /** + * Retrieve the ID/Name of the block. + * + * @return string + * @since 1.0.0 + * @version 1.0.0 + */ + public function get_block_id() { + return sprintf( '%1$s/%2$s', $this->vendor, $this->id ); + } + + /** + * Output a message when no HTML was rendered + * + * @since 1.0.0 + * @since 1.8.0 Don't output empty render messages on the frontend. + * + * @return string + */ + public function get_empty_render_message() { + if ( ! is_admin() ) { + return ''; + } + return __( 'No HTML was returned.', 'lifterlms' ); + } + + /** + * Retrieve a string which can be used to render the block. + * + * @return string + * @since 1.0.0 + * @version 1.0.0 + */ + public function get_render_hook() { + return sprintf( '%1$s_%2$s_block_render', $this->vendor, $this->id ); + } + + /** + * Removed hooks stub. + * Extending classes can use this class to remove hooks attached to the render function action. + * + * @return void + * @since 1.0.0 + * @version 1.0.0 + */ + public function remove_hooks() {} + + /** + * Renders the block type output for given attributes. + * + * @param array $attributes Optional. Block attributes. Default empty array. + * @param string $content Optional. Block content. Default empty string. + * @return string + * @since 1.0.0 + * @version 1.0.0 + */ + public function render_callback( $attributes = array(), $content = '' ) { + + $this->add_hooks( $attributes, $content ); + + ob_start(); + do_action( $this->get_render_hook(), $attributes, $content ); + $ret = ob_get_clean(); + + $this->remove_hooks(); + + if ( ! $ret ) { + $ret = $this->get_empty_render_message(); + } + + return $ret; + + } + + /** + * Register meta attributes stub. + * + * Called after registering the block type. + * + * @return void + * @since 1.0.0 + * @version 1.0.0 + */ + public function register_meta() {} + +} diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-assets.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-assets.php new file mode 100644 index 0000000000..2b7ae5fbfc --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-assets.php @@ -0,0 +1,190 @@ +<?php +/** + * Enqueue assets + * + * Enqueue CSS/JS of all the blocks. + * + * @package LifterLMS_Blocks/Main + * + * @since 1.0.0 + * @version 2.3.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Enqueue assets + * + * @since 1.0.0 + */ +class LLMS_Blocks_Assets { + + /** + * Instances of `LLMS_Assets` + * + * @var null + */ + public $assets; + + /** + * Constructor + * + * @since 1.0.0 + * @since 1.8.0 Stop outputting editor CSS on the frontend. + * @since 1.10.0 Load `LLMS_Assets` and define plugin assets. + * @since 2.0.0 Maybe define backwards compatibility script. + * @since 2.1.0 Adjust `editor_assets()` priority from 999 to 5. + * + * @return void + */ + public function __construct() { + + // Load an instance of the LLMS_Assets class. + $this->assets = new LLMS_Assets( + 'llms-blocks', + array( + // Base defaults shared by all asset types. + 'base' => array( + 'base_file' => LLMS_BLOCKS_PLUGIN_FILE, + 'base_url' => LLMS_BLOCKS_PLUGIN_DIR_URL, + 'version' => LLMS_BLOCKS_VERSION, + 'suffix' => '', // Only minified files are distributed. + ), + // Script specific defaults. + 'script' => array( + 'translate' => true, // All scripts in the blocks plugin are translated. + ), + ) + ); + + // Define plugin assets. + $this->define(); + $this->define_bc(); + + // Enqueue editor assets. + add_action( 'enqueue_block_editor_assets', array( $this, 'editor_assets' ), 5 ); + + } + + /** + * Define block plugin assets. + * + * @since 1.10.0 + * + * @return void + */ + private function define() { + + $asset = include LLMS_BLOCKS_PLUGIN_DIR . '/assets/js/llms-blocks.asset.php'; + + $this->assets->define( + 'scripts', + array( + 'llms-blocks-editor' => array( + 'dependencies' => $asset['dependencies'], + 'file_name' => 'llms-blocks', + 'version' => $asset['version'], + ), + ) + ); + + $this->assets->define( + 'styles', + array( + 'llms-blocks-editor' => array( + 'dependencies' => array( 'wp-edit-blocks' ), + 'file_name' => 'llms-blocks', + 'version' => $asset['version'], + ), + ) + ); + + } + + /** + * Define backwards compatibility assets + * + * @since 2.0.0 + * + * @return void + */ + protected function define_bc() { + + if ( ! $this->use_bc_assets() ) { + return; + } + + $asset = include LLMS_BLOCKS_PLUGIN_DIR . '/assets/js/llms-blocks-backwards-compat.asset.php'; + + $this->assets->define( + 'scripts', + array( + 'llms-blocks-editor-bc' => array( + 'dependencies' => $asset['dependencies'], + 'file_name' => 'llms-blocks-backwards-compat', + 'version' => $asset['version'], + ), + ) + ); + + } + + /** + * Enqueue block editor assets. + * + * @since 1.0.0 + * @since 1.4.1 Fix double slash in asset path. + * @since 1.8.0 Update asset paths and improve script dependencies. + * @since 1.10.0 Use `LLMS_Assets` class methods for asset enqueues. + * @since 2.0.0 Maybe load backwards compatibility script. + * @since 2.2.0 Only load assets on post screens. + * @since 2.3.0 Also load assets on site editor screen. + * + * @return void + */ + public function editor_assets() { + + $screen = get_current_screen(); + if ( $screen && ! in_array( $screen->base, array( 'post', 'site-editor' ), true ) ) { + return; + } + + if ( $this->use_bc_assets() ) { + $this->assets->enqueue_script( 'llms-blocks-editor-bc' ); + } + + $this->assets->enqueue_script( 'llms-blocks-editor' ); + $this->assets->enqueue_style( 'llms-blocks-editor' ); + + } + + /** + * Determines if WP Core backwards compatibility scripts should defined & be loaded. + * + * @since 2.0.0 + * + * @return boolean + */ + private function use_bc_assets() { + return ( ! LLMS_Forms::instance()->are_requirements_met() && + /** + * Filter allowing opt-out of block editor backwards compatibility scripts. + * + * @since 2.0.0 + * + * @example + * ``` + * // Disable backwards compatibility scripts. + * add_filter( 'llms_blocks_load_bc_scripts', '__return_false' ); + * ``` + * + * @param boolean $load_scripts Whether or not to load the scripts. + */ + apply_filters( 'llms_blocks_load_bc_scripts', true ) + ); + } + + +} + +return new LLMS_Blocks_Assets(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-migrate.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-migrate.php new file mode 100644 index 0000000000..db8f5ccac9 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-migrate.php @@ -0,0 +1,433 @@ +<?php +/** + * Handle post migration to the block editor. + * + * @package LifterLMS_Blocks/Classes + * + * @since 1.0.0 + * @version 1.9.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Handle post migration to the block editor. + * + * @since 1.0.0 + * @since 1.4.0 Added action for unmigrating posts from the Tools & Utilities status screen. + * @since 1.7.0 Perform migrations on `current_screen` instead of `admin_enqueue_scripts`. + * Migrate membership post types to use pricing table blocks. + * @since 1.8.0 Update course progress bar shortcode in the course block template. + * @since 1.9.1 Fix course progress block. + */ +class LLMS_Blocks_Migrate { + + /** + * Constructor. + * + * @since 1.0.0 + * @since 1.4.0 Add action for unmigrating posts from the Tools & Utilities status screen. + * @since 1.7.0 Perform migrations on `current_screen` instead of `admin_enqueue_scripts`. + */ + public function __construct() { + + add_action( 'current_screen', array( $this, 'migrate_post' ) ); + add_action( 'wp', array( $this, 'remove_template_hooks' ) ); + add_action( 'llms_blocks_unmigrate_posts', array( $this, 'unmigrate_posts' ) ); + + add_filter( 'llms_blocks_is_post_migrated', array( __CLASS__, 'check_sales_page' ), 15, 2 ); + + } + + /** + * Add a template to a post & save migration status in postmeta. + * + * @since 1.4.0 + * + * @param WP_Post $post Post object. + * @return bool + */ + private function add_template_to_post( $post ) { + + // Update the post. + if ( $this->update_post_content( $post->ID, $post->post_content . "\r\r" . $this->get_template( $post->post_type ) ) ) { + // Save migration state. + $this->update_migration_status( $post->ID ); + return true; + } + + return false; + + } + + /** + * Don't remove core template actions when a sales page is enabled and the page is restricted. + + * @since 1.2.0 + * @since 1.3.1 Unknown. + * + * @param bool $ret Default migration status. + * @param int $post_id WP_Post ID. + * @return bool + */ + public static function check_sales_page( $ret, $post_id ) { + + $page_restricted = llms_page_restricted( $post_id ); + if ( $page_restricted['is_restricted'] ) { + $sales_page = get_post_meta( $post_id, '_llms_sales_page_content_type', true ); + if ( '' === $sales_page || 'content' === $sales_page ) { + $ret = false; + } + } + + return $ret; + + } + + /** + * Get an array of post types which can be migrated. + * + * @since 1.3.3 + * @since 1.7.0 Memberships are migrateable. + * + * @return array + */ + public function get_migrateable_post_types() { + /** + * Filters the post types that can be migrated + * + * @since 1.3.3 + * + * @param string[] $post_types An array of string representing the post types that can be migrated. + */ + return apply_filters( 'llms_blocks_migrateable_post_types', array( 'course', 'lesson', 'llms_membership' ) ); + } + + /** + * Retrieve a WP_Query for posts which have already been migrated. + * + * @since 1.4.0 + * + * @param array $args Optional query arguments to pass to WP_Query. + * @return WP_Query + */ + public function get_migrated_posts( $args = array() ) { + + return new WP_Query( + wp_parse_args( + $args, + array( + 'post_type' => $this->get_migrateable_post_types(), + 'meta_query' => array( + array( + 'key' => '_llms_blocks_migrated', + 'value' => 'yes', + ), + ), + ) + ) + ); + + } + + /** + * Retrieve the block template by post type. + * + * @since 1.0.0 + * @since 1.7.0 Add membership template. + * @since 1.8.0 Updated course progress shortcode and added the `$merge_deprecated_versions` param. + * @since 1.9.1 Fix course progress block. + * + * @param string $post_type WP post type. + * @param boolean $merge_deprecated_versions Optional. Whether or not getting the deprecated blocks merged, useful when removing templates. Default `false`. + * @return string + */ + private function get_template( $post_type, $merge_deprecated_versions = false ) { + + if ( 'course' === $post_type ) { + ob_start(); + + ?><!-- wp:llms/course-information /--> + +<!-- wp:llms/instructors /--> + +<!-- wp:llms/pricing-table /--> + +<!-- wp:llms/course-progress /--> + <?php if ( $merge_deprecated_versions ) : ?> + +<!-- wp:llms/course-progress --> +<div class="wp-block-llms-course-progress">[lifterlms_course_progress check_enrollment=1]</div> +<div class="wp-block-llms-course-progress">[lifterlms_course_progress]</div> +<!-- /wp:llms/course-progress --> + <?php endif; ?> + +<!-- wp:llms/course-continue-button --> +<div class="wp-block-llms-course-continue-button" style="text-align:center">[lifterlms_course_continue_button]</div> +<!-- /wp:llms/course-continue-button --> + +<!-- wp:llms/course-syllabus /--> + <?php + + return ob_get_clean(); + + } + + if ( 'lesson' === $post_type ) { + ob_start(); + ?> + <!-- wp:llms/lesson-progression /--> + +<!-- wp:llms/lesson-navigation /--> + <?php + + return ob_get_clean(); + } + + if ( 'llms_membership' ) { + + ob_start(); + ?> + <!-- wp:llms/pricing-table /--> + <?php + return ob_get_clean(); + + } + + return ''; + + } + + /** + * Migrate posts created prior to the block editor to have default LifterLMS templates + * + * @since 1.0.0 + * @since 1.4.0 Moves content updating methods to it's own function. + * @since 1.7.0 Add Membership post type support. + * Update to handle being triggered by hook `current_screen` instead of `admin_enqueue_scripts`. + * + * @return void + */ + public function migrate_post() { + + global $pagenow; + + if ( 'post.php' !== $pagenow ) { + return; + } + + $post_id = llms_filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT ); + $post = $post_id ? get_post( $post_id ) : false; + + if ( ! $post || ! $this->should_migrate_post( $post->ID ) ) { + return; + } + + if ( 'llms_membership' === $post->post_type ) { + if ( has_block( 'llms/pricing-table', $post->post_content ) ) { + $this->update_migration_status( $post->ID ); + return; + } + } elseif ( has_blocks( $post->post_content ) ) { + $this->update_migration_status( $post->ID ); + return; + } + + $this->add_template_to_post( $post ); + + // Reload. + wp_safe_redirect( + add_query_arg( + array( + 'post' => $post->ID, + 'action' => 'edit', + ), + admin_url( 'post.php' ) + ) + ); + exit; + + } + + /** + * Remove post type templates and any LifterLMS Blocks from a given post. + * + * @since 1.4.0 + * @since 1.8.0 Get all post type's template with deprecated blocks versions merged. + * + * @param WP_Post $post Post object. + * @return bool + */ + private function remove_template_from_post( $post ) { + + $template = $this->get_template( $post->post_type, true ); + if ( ! $template ) { + return; + } + + // explicitly remove template pieces. + $parts = array_filter( array_map( 'trim', explode( "\n", $template ) ) ); + $content = str_replace( $parts, '', $post->post_content ); + + // replace any remaining LLMS blocks not found in the template (also grabs any openers that have block settings JSON). + $content = trim( preg_replace( '#<!-- \/?wp:llms\/.* \/?-->#', '', $content ) ); + + if ( $this->update_post_content( $post->ID, $content ) ) { + $this->update_migration_status( $post->ID, 'no' ); + return true; + } + + return false; + + } + + /** + * Removes core template action hooks from posts which have been migrated to the block editor + * + * @since 1.3.2 Unknown. + * + * @return void + * @since 1.1.0 + */ + public function remove_template_hooks() { + + if ( ! llms_blocks_is_post_migrated( get_the_ID() ) ) { + return; + } + + // Course Information. + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_start', 5 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_length', 10 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_difficulty', 20 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tracks', 25 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_categories', 30 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tags', 35 ); + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_end', 50 ); + + // Remove Course Progress. + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_progress', 60 ); + + // Course Syllabus. + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_syllabus', 90 ); + + // Instructors. + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_course_author', 40 ); + + // Lesson Navigation. + remove_action( 'lifterlms_single_lesson_after_summary', 'lifterlms_template_lesson_navigation', 20 ); + + // Lesson Progression. + remove_action( 'lifterlms_single_lesson_after_summary', 'lifterlms_template_complete_lesson_link', 10 ); + + // Pricing Table. + remove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_pricing_table', 60 ); + remove_action( 'lifterlms_single_membership_after_summary', 'lifterlms_template_pricing_table', 10 ); + + } + + /** + * Determine if a post should be migrated. + * + * @since 1.3.3 + * + * @param int $post_id WP_Post ID. + * @return bool + */ + public function should_migrate_post( $post_id ) { + + $ret = true; + + // Not a valid post type. + if ( ! in_array( get_post_type( $post_id ), $this->get_migrateable_post_types(), true ) ) { + + $ret = false; + + // Classic is enabled, don't migrate. + } elseif ( llms_blocks_is_classic_enabled_for_post( $post_id ) ) { + + $ret = false; + + // Already Migrated. + } elseif ( llms_parse_bool( get_post_meta( $post_id, '_llms_blocks_migrated', true ) ) ) { + + $ret = false; + + } + + /** + * Filters whether or not a post should be migrated + * + * @since 1.3.3 + * + * @param bool $migrate Whether or not a post should be migrated. + * @param int $post_id WP_Post ID. + */ + return apply_filters( 'llms_blocks_should_migrate_post', $ret, $post_id ); + + } + + /** + * Unmigrates migrated posts. + * + * @since 1.4.0 + * + * @return void. + */ + public function unmigrate_posts() { + + $posts = $this->get_migrated_posts( array( 'posts_per_page' => 250 ) ); // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page + + if ( $posts->posts ) { + foreach ( $posts->posts as $post ) { + $this->remove_template_from_post( $post ); + } + } + + } + + /** + * Update post meta data to signal status of the editor migration. + * + * @since 1.1.0 + * + * @param int $post_id WP_Post ID. + * @param string $status Yes or no. + * @return void + */ + private function update_migration_status( $post_id, $status = 'yes' ) { + update_post_meta( $post_id, '_llms_blocks_migrated', $status ); + } + + /** + * Update the post content for a given post. + * + * @since 1.4.0 + * + * @param int $id WP_Post ID. + * @param string $content Post content to update. + * @return bool + */ + private function update_post_content( $id, $content ) { + + global $wpdb; + $update = $wpdb->update( + $wpdb->posts, + array( + 'post_content' => $content, + ), + array( + 'ID' => $id, + ), + array( '%s' ), + array( '%d' ) + ); // db no-cache okay. + + return false === $update ? false : true; + + } + +} + +global $llms_blocks_migrate; +$llms_blocks_migrate = new LLMS_Blocks_Migrate(); +return $llms_blocks_migrate; diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-page-builders.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-page-builders.php new file mode 100644 index 0000000000..a635193b2c --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-page-builders.php @@ -0,0 +1,133 @@ +<?php +/** + * Adds support for page builder plugins.. + * + * @package LifterLMS_Blocks/Classes + * @since 1.2.0 + * @version 1.3.4 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Blocks_Page_Builders class.. + */ +class LLMS_Blocks_Page_Builders { + + /** + * Constructor. + * + * @since 1.2.0 + * @version 1.2.0 + */ + public static function init() { + + add_action( 'init', array( __CLASS__, 'add_filters' ) ); + + } + + /** + * Add filters to support various page builder plugins. + * + * @return void + * @since 1.2.0 + * @version 1.3.4 + */ + public static function add_filters() { + + if ( defined( 'FL_BUILDER_VERSION' ) ) { + add_filter( 'llms_blocks_is_post_migrated', array( __CLASS__, 'check_for_beaver' ), 15, 2 ); + } elseif ( defined( 'ELEMENTOR_VERSION' ) ) { + add_filter( 'llms_blocks_is_post_migrated', array( __CLASS__, 'check_for_elementor' ), 15, 2 ); + } elseif ( defined( 'ET_BUILDER_VERSION' ) ) { + add_filter( 'llms_blocks_is_classic_enabled_for_post', array( __CLASS__, 'check_for_divi_classic' ), 15, 2 ); + add_filter( 'llms_blocks_is_post_migrated', array( __CLASS__, 'check_for_divi' ), 15, 2 ); + } + + } + + /** + * Add support for Beaver Builder. + * If the builder is enabled for the post LifterLMS should treat the post as not migrated (actions are not removed). + * + * @param bool $val default value of the migration status. + * @param int $post_id WP_Post ID. + * @return bool + * @since 1.2.0 + * @version 1.2.0 + */ + public static function check_for_beaver( $val, $post_id ) { + + // If Beaver Builder is enabled for the post, don't remove actions. + if ( FLBuilderModel::is_builder_enabled( $post_id ) ) { + $val = false; + } + + return $val; + + } + + /** + * Add support for Beaver Builder. + * If the builder is enabled for the post LifterLMS should treat the post as not migrated (actions are not removed). + * + * @param bool $val default value of the migration status. + * @param int $post_id WP_Post ID. + * @return bool + * @since 1.2.0 + * @version 1.2.0 + */ + public static function check_for_elementor( $val, $post_id ) { + + // If Elementor builder is enabled for the post, don't remove actions. + if ( 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) { + $val = false; + } + + return $val; + + } + + /** + * Add support for Divi (ET) Builder. + * If the builder is enabled for the post LifterLMS should treat the post as not migrated (actions are not removed). + * + * @param bool $val default value of the migration status. + * @param int $post_id WP_Post ID. + * @return bool + * @since 1.2.0 + * @version 1.2.0 + */ + public static function check_for_divi( $val, $post_id ) { + + // If Divi builder is enabled for the post, don't remove actions. + if ( 'on' === get_post_meta( $post_id, '_et_pb_use_builder', true ) ) { + $val = false; + } + + return $val; + + } + + /** + * If the Divi "Enable Classic Editor" builder setting is enabled then classic is enabled for our purposes. + * + * @param bool $val default value. + * @param mixed $post WP_Post or WP_Post ID. + * @return bool + * @since 1.3.4 + * @version 1.3.4 + */ + public static function check_for_divi_classic( $val, $post ) { + + if ( 'on' === et_get_option( 'et_enable_classic_editor', 'off' ) ) { + $val = true; + } + + return $val; + + } + +} + +return LLMS_Blocks_Page_Builders::init(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-post-instructors.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-instructors.php new file mode 100644 index 0000000000..aa3c34c18b --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-instructors.php @@ -0,0 +1,183 @@ +<?php +/** + * Handle course & membership instructors data. + * + * @package LifterLMS_Blocks/Classes + * + * @since 1.0.0 + * @version 1.7.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Handle course & membership instructors data. + * + * @since 1.0.0 + * @since 1.6.0 Automatically store course/membership instructor with `post_author` data when the post is created. + * @since 1.7.1 Fix Core 5.3 compatibility issues with the `instructors` rest field. + */ +class LLMS_Blocks_Post_Instructors { + + /** + * Supported Post Types. + * + * @var array + */ + protected $post_types = array( 'course', 'llms_membership' ); + + /** + * Constructor. + * + * @since 1.0.0 + * + * @return void + */ + public function __construct() { + + add_action( 'init', array( $this, 'register_meta' ) ); + add_action( 'save_post_course', array( $this, 'maybe_set_default_instructor' ), 50, 3 ); + add_action( 'save_post_llms_membership', array( $this, 'maybe_set_default_instructor' ), 50, 3 ); + + } + + /** + * Meta field update authorization callback. + * + * @since 1.0.0 + * + * @param bool $allowed Is the update allowed. + * @param string $meta_key Meta keyname. + * @param int $object_id WP Object ID (post,comment,etc)... + * @param int $user_id WP User ID. + * @param string $cap requested capability. + * @param array $caps user capabilities. + * @return bool + */ + public function authorize_callback( $allowed, $meta_key, $object_id, $user_id, $cap, $caps ) { + return user_can( $user_id, 'edit_post', $object_id ); + } + + /** + * Retrieve instructor information for a give object. + * + * @since 1.0.0 + * + * @param array $obj Assoc. array of WP_Post data. + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|object Object containing the meta values by name, otherwise WP_Error object. + */ + public function get_callback( $obj, $request ) { + + $ret = array(); + + $obj = llms_get_post( $obj['id'] ); + if ( $obj ) { + $ret = $obj->instructors()->get_instructors( false ); + foreach ( $ret as &$instructor ) { + $name = ''; + $student = llms_get_student( $instructor['id'] ); + if ( $student ) { + $name = $student->get_name(); + } + $instructor['name'] = $name; + } + } + return $ret; + + } + + /** + * Automatically sets instructor data when a new course/membership is created. + * + * @since 1.6.0 + * + * @link https://developer.wordpress.org/reference/hooks/save_post_post-post_type/ + * + * @param int $post_id WP_Post ID. + * @param WP_Post $post Post object. + * @param bool $update Whether the save is an update (`true`) or a creation (`false`). + * @return bool + */ + public function maybe_set_default_instructor( $post_id, $post, $update ) { + + if ( $update || ! $post->post_author ) { + return false; + } + + $obj = llms_get_post( $post ); + if ( ! $obj || ! is_a( $obj, 'LLMS_Post_Model' ) || ! in_array( $obj->get( 'type' ), $this->post_types, true ) ) { + return false; + } + + remove_action( 'save_post_course', array( $this, 'maybe_set_instructors' ), 50, 3 ); + $obj->instructors()->set_instructors( array( array( 'id' => $post->post_author ) ) ); + + return true; + + } + + /** + * Update instructor information for a given object. + * + * @since 1.0.0 + * @since 1.7.1 Decode JSON prior to saving. + * + * @param string $value Instructor data to add to the object (JSON). + * @param WP_Post $object WP_Post object. + * @param string $key name of the field. + * @return null|WP_Error + */ + public function update_callback( $value, $object, $key ) { + + if ( ! current_user_can( 'edit_post', $object->ID ) ) { + return new WP_Error( + 'rest_cannot_update', + __( 'Sorry, you are not allowed to edit the object instructors.', 'lifterlms' ), + array( + 'key' => $name, + 'status' => rest_authorization_required_code(), + ) + ); + } + + $value = json_decode( $value, true ); + + $obj = llms_get_post( $object ); + if ( $obj ) { + $obj->instructors()->set_instructors( $value ); + } + + return null; + } + + /** + * Register custom meta fields. + * + * @since 1.0.0 + * @since 1.6.0 Use `$this->post_types` for loop. + * @since 1.7.1 Don't define a schema. + * + * @return void + */ + public function register_meta() { + + foreach ( $this->post_types as $post_type ) { + + register_rest_field( + $post_type, + 'instructors', + array( + 'get_callback' => array( $this, 'get_callback' ), + 'update_callback' => array( $this, 'update_callback' ), + 'schema' => null, + ) + ); + + } + + } + +} + +return new LLMS_Blocks_Post_Instructors(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-post-types.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-types.php new file mode 100644 index 0000000000..4fe0c64dab --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-types.php @@ -0,0 +1,155 @@ +<?php +/** + * Modify LifterLMS Custom Post Types for Gutenberg editor compatibility + * + * @package LifterLMS_Blocks/Main + * + * @since 1.0.0 + * @version 1.11.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Setup editor templates for LifterLMS custom Post Types + * + * @since 1.0.0 + * @since 1.5.2 Only `show_in_rest` for authenticated users with the `lifterls_instructor` capability. + * @since 1.7.0 Add membership categories and tags to WordPress REST API. + * Add membership post type editor template. + */ +class LLMS_Blocks_Post_Types { + + /** + * Constructor + * + * @since 1.0.0 + * @since 1.7.0 Add membership categories and tags to WordPress REST API. + * Add membership post type editor template. + * + * @return void + */ + public function __construct() { + + // Enable REST API for custom post types. + add_filter( 'lifterlms_register_post_type_course', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_post_type_lesson', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_post_type_membership', array( $this, 'enable_rest' ), 5 ); + + // Enable REST API for custom post taxonomies. + add_filter( 'lifterlms_register_taxonomy_args_course_cat', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_taxonomy_args_course_tag', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_taxonomy_args_course_track', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_taxonomy_args_course_difficulty', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_taxonomy_args_membership_cat', array( $this, 'enable_rest' ), 5 ); + add_filter( 'lifterlms_register_taxonomy_args_membership_tag', array( $this, 'enable_rest' ), 5 ); + + // Setup block editor templates. + add_filter( 'lifterlms_register_post_type_course', array( $this, 'add_course_template' ), 5 ); + add_filter( 'lifterlms_register_post_type_membership', array( $this, 'add_membership_template' ), 5 ); + add_filter( 'lifterlms_register_post_type_lesson', array( $this, 'add_lesson_template' ), 5 ); + + } + + /** + * Enable the rest API for custom post types & taxonomies + * + * @since 1.0.0 + * @since 1.5.2 Only `show_in_rest` for authenticated users with the `lifterls_instructor` capability. + * + * @param array $data post type / taxonomy data. + * @return array + */ + public function enable_rest( $data ) { + + if ( current_user_can( 'lifterlms_instructor' ) ) { + $data['show_in_rest'] = true; + } + + return $data; + + } + + /** + * Add an editor template for courses + * + * @param array $post_type post type data. + * @return array + * @since 1.0.0 + * @version 1.0.0 + */ + public function add_course_template( $post_type ) { + + $post_type['template'] = array( + array( + 'core/paragraph', + array( + 'placeholder' => __( 'Add a short description of your course visible to all visitors...', 'lifterlms' ), + ), + ), + array( 'llms/course-information' ), + array( 'llms/instructors' ), + array( 'llms/pricing-table' ), + array( 'llms/course-progress' ), + array( 'llms/course-continue-button' ), + array( 'llms/course-syllabus' ), + ); + + return $post_type; + + } + + /** + * Add an editor template for memberships. + * + * @since 1.7.0 + * @since 1.11.0 Add instructors block. + * + * @param array $post_type Post type registration data. + * @return array + */ + public function add_membership_template( $post_type ) { + + $post_type['template'] = array( + array( + 'core/paragraph', + array( + 'placeholder' => __( 'Add a short description of your membership visible to all visitors...', 'lifterlms' ), + ), + ), + array( 'llms/instructors' ), + array( 'llms/pricing-table' ), + ); + + return $post_type; + + } + + /** + * Add an editor template for lessons + * + * @param array $post_type post type data. + * @return array + * @since 1.0.0 + * @version 1.0.0 + */ + public function add_lesson_template( $post_type ) { + + $post_type['template'] = array( + array( + 'core/paragraph', + array( + 'placeholder' => __( 'Add your lesson content...', 'lifterlms' ), + ), + ), + array( 'llms/lesson-progression' ), + array( 'llms/lesson-navigation' ), + ); + + return $post_type; + + } + +} + +return new LLMS_Blocks_Post_Types(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-post-visibility.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-visibility.php new file mode 100644 index 0000000000..cd3b3efe05 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-post-visibility.php @@ -0,0 +1,134 @@ +<?php +/** + * Handle course & membership catalog visibility data data. + * + * @package LifterLMS_Blocks/Classes + * @since 1.3.0 + * @version 1.3.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Blocks_Post_Visibility class. + */ +class LLMS_Blocks_Post_Visibility { + + /** + * Constructor. + * + * @since 1.3.0 + * @version 1.3.0 + */ + public function __construct() { + + add_action( 'init', array( $this, 'register_meta' ) ); + + } + + /** + * Meta field update authorization callback. + * + * @param bool $allowed Is the update allowed. + * @param string $meta_key Meta keyname. + * @param int $object_id WP Object ID (post,comment,etc)... + * @param int $user_id WP User ID. + * @param string $cap requested capability. + * @param array $caps user capabilities. + * @return bool + * @since 1.3.0 + * @version 1.3.0 + */ + public function authorize_callback( $allowed, $meta_key, $object_id, $user_id, $cap, $caps ) { + return user_can( $user_id, 'edit_post', $object_id ); + } + + /** + * Retrieve visibility information for a give object. + * + * @param array $obj Assoc. array of WP_Post data. + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|string Visibility term slug or WP_Error object. + * @since 1.3.0 + * @version 1.3.0 + */ + public function get_callback( $obj, $request ) { + + $ret = array(); + + $obj = new LLMS_Product( $obj['id'] ); + if ( $obj ) { + $ret = $obj->get_catalog_visibility(); + } + return $ret; + + } + + /** + * Update visibility information for a given object. + * + * @param string $value new visibility status value. + * @param WP_Post $object WP_Post object. + * @param string $key name of the field. + * @return null|WP_Error + * @since 1.3.0 + * @version 1.3.0 + */ + public function update_callback( $value, $object, $key ) { + + if ( ! current_user_can( 'edit_post', $object->ID ) ) { + return new WP_Error( + 'rest_cannot_update', + __( 'Sorry, you are not allowed to edit the object visibility.', 'lifterlms' ), + array( + 'key' => $name, + 'status' => rest_authorization_required_code(), + ) + ); + } + + $obj = new LLMS_Product( $object->ID ); + if ( $obj ) { + $obj->set_catalog_visibility( $value ); + } + + return null; + } + + /** + * Register custom meta fields. + * + * @return void + * @since 1.3.0 + * @version 1.3.0 + */ + public function register_meta() { + + foreach ( array( 'course', 'llms_membership' ) as $post_type ) { + + register_rest_field( + $post_type, + 'visibility', + array( + 'get_callback' => array( $this, 'get_callback' ), + 'update_callback' => array( $this, 'update_callback' ), + 'schema' => array( + 'description' => __( 'Post visibility.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array(), + 'arg_options' => array( + 'sanitize_callback' => null, + 'validate_callback' => null, + ), + ), + ) + ); + + } + + } + +} + +return new LLMS_Blocks_Post_Visibility(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-reusable.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-reusable.php new file mode 100644 index 0000000000..3a39b7e6e6 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-reusable.php @@ -0,0 +1,200 @@ +<?php +/** + * LLMS_Blocks_Reusable class file + * + * @package LifterLMS_Blocks/Classes + * + * @since 2.0.0 + * @version 2.3.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Manage customizations to reusable blocks + * + * @since 2.0.0 + */ +class LLMS_Blocks_Reusable { + + /** + * Constructor + * + * @since 2.0.0 + * + * @return void + */ + public function __construct() { + + add_action( 'rest_api_init', array( $this, 'rest_register_fields' ) ); + add_filter( 'rest_wp_block_query', array( $this, 'mod_wp_block_query' ), 20, 2 ); + + } + + /** + * Read rest field read callback + * + * @since 2.0.0 + * + * @param array $obj Associative array representing the `wp_block` post. + * @param WP_REST_Request $request Request object. + * @return WP_Error|array Error when current user isn't authorized to read the data or the post association array on success. + */ + public function rest_callback_get( $obj, $request ) { + return llms_parse_bool( get_post_meta( $obj['id'], '_is_llms_field', true ) ) ? 'yes' : 'no'; + } + + /** + * Rest field update callback + * + * @since 2.0.0 + * + * @param array $value Post association array. + * @param WP_Post $obj Post object for the `wp_block` post. + * @param string $key Field key. + * @return WP_Error|boolean Returns an error object when current user lacks permission to update the form or `true` on success. + */ + public function rest_callback_update( $value, $obj, $key ) { + $value = llms_parse_bool( $value ) ? 'yes' : 'no'; + return update_post_meta( $obj->ID, '_is_llms_field', $value ) ? true : false; + } + + /** + * Register custom rest fields + * + * @since 2.0.0 + * + * @return void + */ + public function rest_register_fields() { + + register_rest_field( + 'wp_block', + 'is_llms_field', + array( + 'get_callback' => array( $this, 'rest_callback_get' ), + 'update_callback' => array( $this, 'rest_callback_update' ), + ) + ); + + } + + /** + * Modify the rest request query used to list reusable blocks within the block editor + * + * Ensures that reusable blocks containing LifterLMS Form Fields can only be inserted/viewed + * in the context that we allow them to be used within. + * + * + When viewing a `wp_block` post, all reusable blocks should be displayed. + * + When viewing an `llms_form` post, only blocks that specify `is_llms_field` as 'yes' can be displayed. + * + When viewing any other post, any post with `is_llms_field` of 'yes' is excluded. + * + * @since 2.0.0 + * + * @see [Reference] + * @link [URL] + * + * @param arrays $args WP_Query arguments. + * @param WP_REST_Request $request Request object. + * @return array + */ + public function mod_wp_block_query( $args, $request ) { + + $referer = $request->get_header( 'referer' ); + $screen = empty( $referer ) ? false : $this->get_screen_from_referer( $referer ); + + // We don't care about the screen or it's a reusable block screen. + if ( empty( $screen ) || 'wp_block' === $screen ) { + return $args; + } + + // Add a meta query if it doesn't already exist. + if ( empty( $args['meta_query'] ) ) { + $args['meta_query'] = array( + 'relation' => 'AND', + ); + } + + // Forms should show only blocks with forms and everything else should exclude blocks with forms. + $include_fields = 'llms_form' === $screen; + $args['meta_query'][] = $this->get_meta_query( $include_fields ); + + return $args; + + } + + /** + * Retrieve a meta query array depending on the post type of the referring rest request + * + * @since 2.0.0 + * + * @param boolean $include_fields Whether or not to include form fields. + * @return array + */ + private function get_meta_query( $include_fields ) { + + // Default query when including fields. + $meta_query = array( + 'key' => '_is_llms_field', + 'value' => 'yes', + ); + + // Excluding fields. + if ( ! $include_fields ) { + + $meta_query = array( + 'relation' => 'OR', + wp_parse_args( + array( + 'compare' => '!=', + ), + $meta_query + ), + array( + 'key' => '_is_llms_field', + 'compare' => 'NOT EXISTS', + ), + ); + } + + return $meta_query; + + } + + /** + * Determine the screen where a reusable blocks rest query originated + * + * The screen name will either be "widgets" or the WP_Post name of a registered WP_Post type. + * + * For any other screen we return `false` because we don't care about it. + * + * @since 2.0.0 + * @since 2.3.1 Don't pass `null` to `basename()`. + * + * @param string $referer Referring URL for the REST request. + * @return string|boolean Returns the screen name or `false` if we don't care about the screen. + */ + private function get_screen_from_referer( $referer ) { + + // Blockified widgets screen. + $url_path = wp_parse_url( $referer, PHP_URL_PATH ); + if ( $url_path && 'widgets.php' === basename( $url_path ) ) { + return 'widgets'; + } + + $query_args = array(); + wp_parse_str( wp_parse_url( $referer, PHP_URL_QUERY ), $query_args ); + + // Something else. + if ( empty( $query_args['post'] ) ) { + return false; + } + + // Block editor for a WP_Post. + return get_post_type( $query_args['post'] ); + + } + +} + +return new LLMS_Blocks_Reusable(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-status-tools.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-status-tools.php new file mode 100644 index 0000000000..66c486f1cb --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-status-tools.php @@ -0,0 +1,88 @@ +<?php +/** + * Add Blocks specific LifterLMS Status Page tools. + * + * @package LifterLMS_Blocks/Admin/Classes + * + * @since 1.4.0 + * @version 1.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Blocks_Status_Tools class. + * + * @since 1.4.0 + */ +class LLMS_Blocks_Status_Tools { + + /** + * Constructor. + * + * @since 1.4.0 + */ + public function __construct() { + + if ( class_exists( 'Classic_Editor' ) ) { + + add_filter( 'llms_status_tools', array( $this, 'add_tools' ) ); + add_action( 'llms_status_tool', array( $this, 'maybe_toggle_mode' ) ); + + } + + } + + /** + * Add status page tools + * + * @since 1.4.0 + * + * @param array $tools array of tools. + * @return array + */ + public function add_tools( $tools ) { + + global $llms_blocks_migrate; + $posts = $llms_blocks_migrate->get_migrated_posts(); + + if ( $posts->found_posts ) { + + $desc = __( 'Removes block editor code from all courses and lessons which were migrated to the block editor during an upgrade to WordPress 5.0 or later. If you installed the Classic Editor plugin after upgrading and see duplicate content items (such as the course syllabus or lesson mark complete button) this tool will remove the duplicates.', 'lifterlms' ); + $desc .= '<br><br>'; + // Translators: %d = number of affected courses/lessons. + $desc .= sprintf( __( 'Currently %d courses and/or lessons are affected.', 'lifterlms' ), $posts->found_posts ); + + $tools['blocks-unmigrate'] = array( + 'description' => $desc, + 'label' => __( 'Remove LifterLMS Block Code', 'lifterlms' ), + 'text' => __( 'Remove Block Code', 'lifterlms' ), + ); + + } + + return $tools; + + } + + /** + * Run tool actions on tool page form submission. + * + * @since 1.4.0 + * + * @param string $tool ID of the tool being run. + * @return void + */ + public function maybe_toggle_mode( $tool ) { + + if ( 'blocks-unmigrate' !== $tool ) { + return; + } + + do_action( 'llms_blocks_unmigrate_posts' ); + + } + +} + +return new LLMS_Blocks_Status_Tools(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks-visibility.php b/libraries/lifterlms-blocks/includes/class-llms-blocks-visibility.php new file mode 100644 index 0000000000..566da13f3a --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks-visibility.php @@ -0,0 +1,258 @@ +<?php +/** + * Manage block visibility options. + * + * @package LifterLMS_Blocks/Classes + * + * @since 1.0.0 + * @version 2.3.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Blocks_Visibility class. + * + * @since 1.0.0 + * @since 1.6.0 Add logic for `logged_in` and `logged_out` block visibility options. + * Adjusted priority of `render_block` filter to 20. + */ +class LLMS_Blocks_Visibility { + + /** + * Constructor. + * + * @since 1.0.0 + * @since 1.6.0 Adjusted priority of `render_block` filter to 20. + * + * @return void + */ + public function __construct() { + add_filter( 'render_block', array( $this, 'maybe_filter_block' ), 20, 2 ); + } + + /** + * Retrieve visibility attributes. + * + * Used when registering dynamic blocks via PHP. + * + * @since 1.0.0 + * + * @return array + */ + public static function get_attributes() { + return array( + 'llms_visibility' => array( + 'default' => 'all', + 'type' => 'string', + ), + 'llms_visibility_in' => array( + 'default' => '', + 'type' => 'string', + ), + 'llms_visibility_posts' => array( + 'default' => '[]', + 'type' => 'string', + ), + ); + } + + /** + * Get the number of enrollments for a user by post type. + * + * @since 1.0.0 + * + * @param int $uid WP_User ID. + * @param string $type Post type. + * @return int + */ + private function get_enrollment_count_by_type( $uid, $type ) { + + $found = 0; + $student = llms_get_student( $uid ); + + $type = str_replace( 'any_', '', $type ); + + if ( 'course' === $type || 'membership' === $type ) { + $enrollments = $student->get_enrollments( $type, array( 'limit' => 1 ) ); + $found = $enrollments['found']; + } elseif ( 'any' === $type ) { + $found = $this->get_enrollment_count_by_type( $uid, 'course' ); + if ( ! $found ) { + $found = $this->get_enrollment_count_by_type( $uid, 'membership' ); + } + } + + return $found; + + } + + /** + * Parse post ids from block visibility in attributes. + * + * @since 1.0.0 + * + * @param array $attrs Block attributes. + * @return array + */ + private function get_post_ids_from_block_attributes( $attrs ) { + + $ids = array(); + if ( 'this' === $attrs['llms_visibility_in'] ) { + $ids[] = get_the_ID(); + } elseif ( ! empty( $attrs['llms_visibility_posts'] ) ) { + $ids = wp_list_pluck( json_decode( $attrs['llms_visibility_posts'] ), 'id' ); + } + + return $ids; + + } + + /** + * Filter block output. + * + * @since 1.0.0 + * @since 1.6.0 Add logic for `logged_in` and `logged_out` block visibility options. + * @since 2.0.0 Added a conditional prior to checking the block's visibility attributes. + * + * @param string $content Block inner content. + * @param array $block Block data array. + * @return string + */ + public function maybe_filter_block( $content, $block ) { + + // Allow conditionally filtering the block based on external context. + if ( ! $this->should_filter_block( $block ) ) { + return $content; + } + + // No attributes or no llms visibility settings (visibile to "all"). + if ( empty( $block['attrs'] ) || empty( $block['attrs']['llms_visibility'] ) ) { + return $content; + } + + $uid = get_current_user_id(); + + // Show only to logged in users. + if ( 'logged_in' === $block['attrs']['llms_visibility'] && ! $uid ) { + + $content = ''; + + // Show only to logged out users. + } elseif ( 'logged_out' === $block['attrs']['llms_visibility'] && $uid ) { + $content = ''; + + // Enrolled checks. + } elseif ( 'enrolled' === $block['attrs']['llms_visibility'] && ! empty( $block['attrs']['llms_visibility_in'] ) ) { + + // Don't have to run any further checks if we don't have a user. + if ( ! $uid ) { + + $content = ''; + + // Checks for the "any" conditions. + } elseif ( in_array( $block['attrs']['llms_visibility_in'], array( 'any', 'any_course', 'any_membership' ), true ) ) { + + $found = $this->get_enrollment_count_by_type( $uid, $block['attrs']['llms_visibility_in'] ); + if ( ! $found ) { + $content = ''; + } + + // Checks for specifics. + } elseif ( in_array( $block['attrs']['llms_visibility_in'], array( 'this', 'list_all', 'list_any' ), true ) ) { + + $relation = 'list_any' === $block['attrs']['llms_visibility_in'] ? 'any' : 'all'; // "this" becomes an "all" relationship + if ( ! llms_is_user_enrolled( $uid, $this->get_post_ids_from_block_attributes( $block['attrs'] ), $relation ) ) { + $content = ''; + } + } + + // Not-Enrolled checks. + } elseif ( 'not_enrolled' === $block['attrs']['llms_visibility'] && ! empty( $block['attrs']['llms_visibility_in'] ) ) { + + // Only need to check logged in users. + if ( $uid ) { + + // Checks for the "any" conditions. + if ( in_array( $block['attrs']['llms_visibility_in'], array( 'any', 'any_course', 'any_membership' ), true ) ) { + + $found = $this->get_enrollment_count_by_type( $uid, $block['attrs']['llms_visibility_in'] ); + if ( $found ) { + $content = ''; + } + + // Checks for specifics. + } elseif ( in_array( $block['attrs']['llms_visibility_in'], array( 'this', 'list_all', 'list_any' ), true ) ) { + + $relation = 'list_any' === $block['attrs']['llms_visibility_in'] ? 'any' : 'all'; // "this" becomes an "all" relationship + if ( llms_is_user_enrolled( $uid, $this->get_post_ids_from_block_attributes( $block['attrs'] ), $relation ) ) { + $content = ''; + } + } + } + } + + /** + * Filters a blocks content after it has been run through visibility attribute filters + * + * @since 1.0.0 + * + * @param string $content The HTML content for a block. May be an empty string if the block should be invisible to the current user. + * @param array $block Block data array. + */ + return apply_filters( 'llms_blocks_visibility_render_block', $content, $block ); + + } + + /** + * Determine whether or not a block's rendering should be modified by block-level visibility settings + * + * This method does not determine whether or not the block will be rendered, it only determines whether + * or not we should check if it should be rendered. + * + * This method is primarily used to ensure that LifterLMS core dynamic blocks (pricing table, course syllabus, etc...) + * are *always* displayed to creators when editing content within the block editor. This parses data from a block-renderer + * WP Core API request. + * + * @since 2.0.0 + * @since 2.3.1 Don't use deprecated `FILTER_SANITIZE_STRING`. + * + * @link https://developer.wordpress.org/rest-api/reference/rendered-blocks/ + * + * @param array $block Block data array. + * @return boolean If `true`, block filters should be checked, other wise they will be skipped. + */ + private function should_filter_block( $block ) { + + // Always filter unless explicitly told not to. + $should_filter = true; + + if ( llms_is_rest() ) { + + $context = llms_filter_input( INPUT_GET, 'context' ); + $post_id = llms_filter_input( INPUT_GET, 'post_id', FILTER_SANITIZE_NUMBER_INT ); + + // Always render blocks when a valid user is requesting the block in the edit context. + if ( 'edit' === $context && $post_id && current_user_can( 'edit_post', $post_id ) ) { + $should_filter = false; + } + } + + /** + * Filters whether or not a block's rendering should be modified by block-level visibility settings + * + * This filter does not determine whether or not the block will be rendered, it only determines whether + * or not we should check if it should be rendered. + * + * @since 2.0.0 + * + * @param boolean $should_filter Whether or not to apply visibility filters. + * @param array $block Block data array. + */ + return apply_filters( 'llms_blocks_visibility_should_filter_block', $should_filter, $block ); + + } + +} + +return new LLMS_Blocks_Visibility(); diff --git a/libraries/lifterlms-blocks/includes/class-llms-blocks.php b/libraries/lifterlms-blocks/includes/class-llms-blocks.php new file mode 100644 index 0000000000..beca425dc3 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/class-llms-blocks.php @@ -0,0 +1,252 @@ +<?php +/** + * Plugin Initialization + * + * @package LifterLMS_Blocks/Classes + * + * @since 1.0.0 + * @version 2.3.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Blocks class + * + * @since 1.0.0 + * @since 2.2.1 Handle '-src' in WordPress version numbers in `init()`. + */ +class LLMS_Blocks { + + /** + * Constructor. + * + * @since 1.0.0 + * @since 1.3.0 Updated. + * @since 1.5.1 Add `admin_print_scripts` hook to handle outputting dynamic block information. + * @since 1.10.0 Load localization files when running as an independent plugin. + * @since 2.0.0 Move action & filter hooks to the the `init()` method. + */ + public function __construct() { + + add_action( 'plugins_loaded', array( $this, 'init' ) ); + + } + + /** + * Add a custom LifterLMS block category + * + * @since 1.0.0 + * @since 1.6.0 Add Form Fields category. + * + * @param array $categories existing block cats. + * @return array + */ + public function add_block_category( $categories ) { + + $categories[] = array( + 'slug' => 'llms-blocks', + 'title' => __( 'LifterLMS Blocks', 'lifterlms' ), + ); + + array_unshift( + $categories, + array( + 'slug' => 'llms-custom-fields', + 'title' => __( 'Custom User Information', 'lifterlms' ), + ) + ); + + array_unshift( + $categories, + array( + 'slug' => 'llms-user-info-fields', + 'title' => __( 'User Information', 'lifterlms' ), + ) + ); + + return $categories; + } + + + /** + * Print dynamic block information as a JS variable. + * + * Allows us to ensure we only add visibility attributes to static blocks. + * Prevents an issue causing rest api validation issues during attribute validation + * because it's impossible to register custom attributes on a block. + * + * @link https://github.com/gocodebox/lifterlms-blocks/issues/30 + * + * @since 1.5.1 + * @since 2.0.0 Since WordPress 5.8 blocks are available in widgets and customizer screen too. + * + * @return void + */ + public function admin_print_scripts() { + + $screen = get_current_screen(); + if ( ! $screen || ( empty( $screen->is_block_editor ) && 'customize' !== $screen->base ) ) { + return; + } + + echo '<script>window.llms.dynamic_blocks = ' . wp_json_encode( $this->get_dynamic_block_names() ) . ';</script>'; + + } + + /** + * Retrieve a list of dynamic block names registered with WordPress (excluding LifterLMS blocks). + * + * @since 1.5.1 + * + * @return array + */ + private function get_dynamic_block_names() { + $blocks = array(); + foreach ( get_dynamic_block_names() as $name ) { + if ( 0 !== strpos( $name, 'llms/' ) ) { + $blocks[] = $name; + } + } + return apply_filters( 'llms_blocks_get_dynamic_block_names', $blocks ); + } + + /** + * Include all files. + * + * @since 2.0.0 + * @since 2.3.0 Include php template block file. + * + * @return void + */ + private function includes() { + + // Functions. + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/functions-llms-blocks.php'; + + // Classes. + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-assets.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-abstract-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-migrate.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-page-builders.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-post-instructors.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-post-types.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-post-visibility.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-reusable.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-status-tools.php'; + + // Block Visibility Component. + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks-visibility.php'; + + // Dynamic Blocks. + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-course-information-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-course-syllabus-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-course-progress-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-instructors-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-lesson-navigation-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-lesson-progression-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-pricing-table-block.php'; + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/blocks/class-llms-blocks-php-template-block.php'; + + } + + /** + * Register all blocks & components. + * + * @since 1.0.0 + * @since 1.4.0 Add status tools class. + * @since 1.9.0 Added course progress block class. + * @since 2.0.0 Return early if LifterLMS isn't installed, move file inclusion to `$this->includes()`, + * and moved actions and filters from the constructor. + * @since 2.2.1 Handle '-src' in WordPress version numbers. + * + * @return void + */ + public function init() { + + if ( ! function_exists( 'llms' ) || ! version_compare( '5.0.0-rc.2', llms()->version, '<=' ) ) { + return; + } + + $this->includes(); + + add_action( 'add_meta_boxes', array( $this, 'remove_metaboxes' ), 999, 2 ); + + global $wp_version; + $filter = version_compare( $wp_version, '5.8-src', '>=' ) ? 'block_categories_all' : 'block_categories'; + + add_filter( $filter, array( $this, 'add_block_category' ) ); + add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ), 15 ); + + /** + * When loaded as a library included by the LifterLMS core localization is handled by the LifterLMS core. + * + * When the plugin is loaded by itself as a plugin, we must localize it independently. + */ + if ( ! defined( 'LLMS_BLOCKS_LIB' ) || ! LLMS_BLOCKS_LIB ) { + add_action( 'init', array( $this, 'load_textdomain' ), 0 ); + } + + } + + /** + * Load l10n files. + * + * This method is only used when the plugin is loaded as a standalone plugin (for development purposes), + * otherwise (when loaded as a library from within the LifterLMS core plugin) the localization + * strings are included into the LifterLMS Core plugin's po/mo files and are localized by the LifterLMS + * core plugin. + * + * Files can be found in the following order (The first loaded file takes priority): + * 1. WP_LANG_DIR/lifterlms/lifterlms-blocks-LOCALE.mo + * 2. WP_LANG_DIR/plugins/lifterlms-blocks-LOCALE.mo + * 3. WP_CONTENT_DIR/plugins/lifterlms-blocks/i18n/lifterlms-blocks-LOCALE.mo + * + * Note: The function `load_plugin_textdomain()` is not used because the same textdomain as the LifterLMS core + * is used for this plugin but the file is named `lifterlms-blocks` in order to allow using a separate language + * file for each codebase. + * + * @since 1.10.0 + * + * @return void + */ + public function load_textdomain() { + + // load locale. + $locale = apply_filters( 'plugin_locale', get_locale(), 'lifterlms' ); + + // Load from the LifterLMS "safe" directory if it exists. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/lifterlms/lifterlms-blocks-' . $locale . '.mo' ); + + // Load from the default plugins language file directory. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/plugins/lifterlms-blocks-' . $locale . '.mo' ); + + // Load from the plugin's language file directory. + load_textdomain( 'lifterlms', LLMS_BLOCKS_PLUGIN_DIR . '/i18n/lifterlms-blocks-' . $locale . '.mo' ); + + } + + /** + * Remove deprecated core metaboxes. + * + * @since 1.0.0 + * @since 1.3.0 Updated. + * + * @param string $post_type WP post type of the current post. + * @param string $post WP_Post. + * @return void + */ + public function remove_metaboxes( $post_type, $post ) { + + if ( ! llms_blocks_is_classic_enabled_for_post( $post ) ) { + + remove_meta_box( 'llms-instructors', 'course', 'normal' ); + remove_meta_box( 'llms-instructors', 'llms_membership', 'normal' ); + + } + + } + +} + +return new LLMS_Blocks(); diff --git a/libraries/lifterlms-blocks/includes/functions-llms-blocks.php b/libraries/lifterlms-blocks/includes/functions-llms-blocks.php new file mode 100644 index 0000000000..9102605455 --- /dev/null +++ b/libraries/lifterlms-blocks/includes/functions-llms-blocks.php @@ -0,0 +1,75 @@ +<?php +/** + * Serverside block compononent registration + * + * @package LifterLMS_Blocks/Functions + * @since 1.3.0 + * @version 1.3.3 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Determine if the Classic Editor is enabled for a given post. + * + * @param mixed $post WP_Post or WP_Post ID. + * @return boolean + * @since 1.3.0 + * @version 1.3.3 + */ +function llms_blocks_is_classic_enabled_for_post( $post ) { + + $ret = false; + + if ( class_exists( 'Classic_Editor' ) ) { + + // Users can choose which editor. + if ( 'allow' === get_option( 'classic-editor-allow-users', 'disallow' ) ) { + + // check the postmeta to determine which editor we're using. + $post = get_post( $post ); + if ( $post ) { + $ret = ( 'classic-editor' === get_post_meta( $post->ID, 'classic-editor-remember', true ) ); + } + + // Uses same editor for all posts. + } else { + + $ret = ( 'classic' === get_option( 'classic-editor-replace', 'classic' ) ); + + } + } + + return apply_filters( 'llms_blocks_is_classic_enabled_for_post', $ret, $post ); + +} + +/** + * Determine if a post is migrated + * + * @param mixed $post WP_Post or WP_Post ID. + * @return boolean + * @since 1.3.1 + * @version 1.3.1 + */ +function llms_blocks_is_post_migrated( $post ) { + + $post_id = null; + $ret = false; + + $post = get_post( $post ); + if ( $post ) { + + $post_id = $post->ID; + + // Classic editor is being used for this post. + if ( llms_blocks_is_classic_enabled_for_post( $post_id ) ) { + $ret = false; + } else { + $ret = llms_parse_bool( get_post_meta( $post_id, '_llms_blocks_migrated', true ) ); + } + } + + return apply_filters( 'llms_blocks_is_post_migrated', $ret, $post_id ); + +} diff --git a/libraries/lifterlms-blocks/includes/index.php b/libraries/lifterlms-blocks/includes/index.php new file mode 100644 index 0000000000..82e2315c6b --- /dev/null +++ b/libraries/lifterlms-blocks/includes/index.php @@ -0,0 +1,2 @@ +<?php +// silence. diff --git a/libraries/lifterlms-blocks/lifterlms-blocks.php b/libraries/lifterlms-blocks/lifterlms-blocks.php new file mode 100644 index 0000000000..f258ad420f --- /dev/null +++ b/libraries/lifterlms-blocks/lifterlms-blocks.php @@ -0,0 +1,63 @@ +<?php +/** + * LifterLMS Blocks Plugin + * + * @package LifterLMS_Blocks/Main + * + * @since 1.0.0 + * @version 2.0.0 + * + * @wordpress-plugin + * Plugin Name: LifterLMS Blocks + * Plugin URI: https://github.com/gocodebox/lifterlms-blocks + * Description: WordPress Editor (Gutenberg) blocks for LifterLMS. + * Version: 2.3.2 + * Author: LifterLMS + * Author URI: https://lifterlms.com/ + * Text Domain: lifterlms + * Domain Path: /i18n + * License: GPLv3 + * License URI: https://www.gnu.org/licenses/gpl-3.0.html + * Requires at least: 5.5 + * Tested up to: 5.9 + */ + +// Restrict Direct Access. +defined( 'ABSPATH' ) || exit; + +// Define Constants. +if ( ! defined( 'LLMS_BLOCKS_VERSION' ) ) { + define( 'LLMS_BLOCKS_VERSION', '2.3.2' ); +} + +/** + * Allows disabling the blocks plugin & functionality. + * + * @since 1.0.0 + * + * @param boolean $load Whether the plugin should be loaded. Defaults to `true`. + */ +if ( ! apply_filters( 'llms_load_blocks_plugin', true ) ) { + return; +} + + +// Load only when the block editor is present. +if ( function_exists( 'has_blocks' ) ) { + + if ( ! defined( 'LLMS_BLOCKS_PLUGIN_FILE' ) ) { + define( 'LLMS_BLOCKS_PLUGIN_FILE', __FILE__ ); + } + + if ( ! defined( 'LLMS_BLOCKS_PLUGIN_DIR' ) ) { + define( 'LLMS_BLOCKS_PLUGIN_DIR', dirname( LLMS_BLOCKS_PLUGIN_FILE ) ); + } + + if ( ! defined( 'LLMS_BLOCKS_PLUGIN_DIR_URL' ) ) { + define( 'LLMS_BLOCKS_PLUGIN_DIR_URL', plugin_dir_url( LLMS_BLOCKS_PLUGIN_FILE ) ); + } + + // Start. + require_once LLMS_BLOCKS_PLUGIN_DIR . '/includes/class-llms-blocks.php'; + +} diff --git a/libraries/lifterlms-cli/CHANGELOG.md b/libraries/lifterlms-cli/CHANGELOG.md new file mode 100644 index 0000000000..f602d6a46d --- /dev/null +++ b/libraries/lifterlms-cli/CHANGELOG.md @@ -0,0 +1,24 @@ +LifterLMS CLI Changelog +======================= + +v0.0.3 - 2021-11-03 +------------------- + ++ Improved help documentation for several commands. ++ Added a warning to the root command's help documentation denoting that the LLMS-CLI is in open public beta and its functionality is subject to change. + + +v0.0.2 - 2021-10-15 +------------------- + ++ Use a strict comparison when checking response status using the `license` command. ++ Remove `--db` option from the `version` command. This will be implemented in a separate command. ++ Fixed an unmerged placeholder in warning message when add-on is not installed when using the `activate`. ++ Updated success message when using `channel set`. ++ Completion messages use says "deactivate(d)" in favor of "activate(d)" in the `addon deactivate` command. + + +v0.0.1 - 2021-07-27 +------------------- + ++ Initial public release diff --git a/libraries/lifterlms-cli/index.php b/libraries/lifterlms-cli/index.php new file mode 100644 index 0000000000..0b2d10de64 --- /dev/null +++ b/libraries/lifterlms-cli/index.php @@ -0,0 +1 @@ +<?php // Silence. diff --git a/libraries/lifterlms-cli/lifterlms-cli.php b/libraries/lifterlms-cli/lifterlms-cli.php new file mode 100644 index 0000000000..c8fa25d339 --- /dev/null +++ b/libraries/lifterlms-cli/lifterlms-cli.php @@ -0,0 +1,60 @@ +<?php +/** + * LifterLMS CLI Plugin + * + * @package LifterLMS/CLI/Main + * + * @since 0.0.1 + * @version 0.0.1 + * + * Plugin Name: LifterLMS CLI + * Plugin URI: https://lifterlms.com/ + * Description: WP CLI feature plugin for the LifterLMS Core. + * Version: 0.0.3 + * Author: LifterLMS + * Author URI: https://lifterlms.com/ + * Text Domain: lifterlms + * Domain Path: /i18n + * License: GPLv3 + * License URI: https://www.gnu.org/licenses/gpl-3.0.html + * Requires LifterLMS: 5.0 + */ + +use LifterLMS\CLI\Main; + +defined( 'ABSPATH' ) || exit; + +// Don't load the CLI. +if ( defined( 'LLMS_CLI_DISABLE' ) && LLMS_CLI_DISABLE ) { + return; +} + +// Only load in CLI context. +if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { + return; +} + +// Define Constants. +if ( ! defined( 'LLMS_CLI_PLUGIN_FILE' ) ) { + define( 'LLMS_CLI_PLUGIN_FILE', __FILE__ ); +} + +if ( ! defined( 'LLMS_CLI_PLUGIN_DIR' ) ) { + define( 'LLMS_CLI_PLUGIN_DIR', dirname( __FILE__ ) . '/' ); +} + +// Autoload. +require_once LLMS_CLI_PLUGIN_DIR . 'vendor/autoload.php'; + +/** + * Main Plugin Instance + * + * @since 0.0.1 + * + * @return LLMS_CLI + */ +function llms_cli() { + return Main::instance(); +} + +return llms_cli(); diff --git a/libraries/lifterlms-cli/src/Commands/AbstractCommand.php b/libraries/lifterlms-cli/src/Commands/AbstractCommand.php new file mode 100644 index 0000000000..e1b42c49d2 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AbstractCommand.php @@ -0,0 +1,88 @@ +<?php +/** + * LLMS_CLI_Abstract_Command file. + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands; + +/** + * Base CLI command for use by LifterLMS CLI commands + * + * @since 0.0.1 + */ +abstract class AbstractCommand extends \WP_CLI_Command { + + /** + * Determines whether or not a command is being chained. + * + * When chaining commands (like `addon uninstall --deactivate`) we skip + * output of the secondary command (deactivate won't output it's success/error). + * + * @var boolean + */ + protected $chaining = false; + + /** + * Chain a command within the class + * + * @since 0.0.1 + * + * @param string $command Method name of the command to chain. + * @param array $args Indexed array of positional command arguments to pass to the chained command. + * @param array $assoc_args Associative array of command options to pass to the chained command. + * @return void + */ + protected function chain_command( $command, $args = array(), $assoc_args = array() ) { + $this->chaining = true; + $this->$command( $args, $assoc_args ); + $this->chaining = false; + } + + /** + * Retrieve an LLMS_Add_On object for a given add-on by it's slug. + * + * @since 0.0.1 + * + * @param string $slug An add-on slug. Must be prefixed. + * @param bool|WP_Error|string $err If truthy, will return `null` and use log to the console using a WP_CLI method as defined by $err_type. + * Pass `true` to output a default error message. + * Pass a WP_Error object or string to use as the error. + * @param string $err_type Method to pass `$err` to when an error is encountered. Default `\WP_CLI::error()`. + * Use `\WP_CLI::warning()` or `\WP_CLI::log()` where appropriate. + * @return LLMS_Add_On|boolean|null Returns an add-on object if the add-on can be located or `false` if not found. + * Returns `null` when an error is encountered and `$err` is a truthy. + */ + protected function get_addon( $slug, $err = false, $err_type = 'error' ) { + + $addon = llms_get_add_on( $this->prefix_slug( $slug ), 'slug' ); + $exists = ! empty( $addon->get( 'id' ) ); + + if ( ! $exists && $err ) { + $err = is_bool( $err ) ? sprintf( 'Invalid slug: %s.', $slug ) : $err; + return \WP_CLI::$err_type( $err ); + } + + return ! $exists ? false : $addon; + } + + /** + * Prefix an add-on slug with `lifterlms-` if it's not already present. + * + * @since 0.0.1 + * + * @param string $slug Add-on slug. + * @return string + */ + protected function prefix_slug( $slug ) { + if ( 0 !== strpos( $slug, 'lifterlms-' ) ) { + $slug = "lifterlms-{$slug}"; + } + return $slug; + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Activate.php b/libraries/lifterlms-cli/src/Commands/AddOn/Activate.php new file mode 100644 index 0000000000..a7973ba0dc --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Activate.php @@ -0,0 +1,101 @@ +<?php +/** + * Addon Activate class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.2 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +/** + * AddOn Activation command + * + * @since 0.0.1 + */ +trait Activate { + + /** + * Activate one or more add-ons. + * + * ## OPTIONS + * + * [<slug>...] + * : The slug of one or more LifterLMS add-on to install. + * + * [--all] + * : If set, all of the LifterLMS add-ons installed on the site will be activated. + * + * ## EXAMPLES + * + * # Activate the LifterLMS Groups add-on. + * $ wp llms addon activate lifterlms-groups + * + * # Activate an add-on without using the `lifterlms-` prefix. + * $ wp llms addon activate advanced-videos + * + * # Activate multiple LifterLMS add-ons. + * $ wp llms addon activate lifterlms-groups lifterlms-assignments lifterlms-pdfs + * + * # Activate all installed LifterLMS add-ons. + * $ wp llms addon activate --all + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function activate( $args, $assoc_args ) { + + if ( ! empty( $assoc_args['all'] ) ) { + $args = $this->get_available_addons( 'inactive', false ); + if ( empty( $args ) ) { + return \WP_CLI::warning( 'No add-ons to activate.' ); + } + } + + $results = $this->loop( $args, $assoc_args, 'activate_one' ); + if ( ! $this->chaining ) { + \WP_CLI\Utils\report_batch_operation_results( 'add-on', 'activate', count( $args ), $results['successes'], $results['errors'] ); + } + + } + + /** + * Loop callback function for activate() + * + * Ensures add-on can be activated and actually activates the add-on. + * + * @since 0.0.1 + * @since 0.0.2 Fixed unmerged placeholder in warning message when add-on is not installed. + * + * @param string $slug Add-on slug. + * @param LLMS_Add_On $addon Add-on object. + * @param array $assoc_args Associative array of command options. + * @return null|true Returns `null` if an error is encountered and `true` on success. + */ + private function activate_one( $slug, $addon, $assoc_args ) { + + if ( $addon->is_active() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%s" is already active.', $slug ) ); + } + + if ( ! $addon->is_installed() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%1$s" is not installed. Run \'wp llms addon install %s\' to install it.', $slug ) ); + } + + $res = $addon->activate(); + if ( is_wp_error( $res ) ) { + return \WP_CLI::warning( $res ); + } + + \WP_CLI::log( $res ); + + return true; + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/ChannelSet.php b/libraries/lifterlms-cli/src/Commands/AddOn/ChannelSet.php new file mode 100644 index 0000000000..256c6fe07f --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/ChannelSet.php @@ -0,0 +1,61 @@ +<?php +/** + * AddOn ChannelSet class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.2 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +/** + * AddOn channel-set command + * + * @since 0.0.1 + */ +trait ChannelSet { + + /** + * Set the update channel subscription for an add-on. + * + * ## OPTIONS + * + * <slug> + * : The slug of the add-on. + * + * [<channel>] + * : The update channel to subscribe to. + * --- + * default: 'stable' + * options: + * - stable + * - beta + * --- + * + * ## EXAMPLES + * + * # Subscribe the Groups add-on to the beta channel. + * $ wp llms addon channel-set lifterlms-groups stable + * + * # Subscribe to the stable channel. + * $ wp llms addon channel-set lifterlms-groups stable + * + * @subcommand channel-set + * + * @since 0.0.1 + * @since 0.0.2 Updated success message. + * + * @param array $args Indexed array of positional command arguments. + * @return null + */ + public function channel_set( $args ) { + + $addon = $this->get_addon( $args[0], true ); + $addon->subscribe_to_channel( $args[1] ); + return \WP_CLI::success( sprintf( 'Subscribed to the %s channel.', $args[1] ) ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Deactivate.php b/libraries/lifterlms-cli/src/Commands/AddOn/Deactivate.php new file mode 100644 index 0000000000..117e898e4b --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Deactivate.php @@ -0,0 +1,111 @@ +<?php +/** + * AddOn Deactivate class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.2 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +/** + * AddOn Activation and deactivation commands + * + * @since 0.0.1 + */ +trait Deactivate { + + /** + * Deactivate one or more plugin add-ons. + * + * ## OPTIONS + * + * [<slug>...] + * : The slug of one or more add-on to deactivate. + * + * [--uninstall] + * : Uninstall the add-ons after deactivation. + * + * [--all] + * : If set, all of the plugin add-ons installed on the site will be activated. + * + * ## EXAMPLES + * + * # Deactivate the LifterLMS Groups add-on. + * $ wp llms addon deactivate lifterlms-groups + * + * # Deactivate an add-on without using the `lifterlms-` prefix. + * $ wp llms addon deactivate advanced-videos + * + * # Deactivate multiple LifterLMS add-ons. + * $ wp llms addon deactivate lifterlms-groups lifterlms-assignments lifterlms-pdfs + * + * # Deactivate all installed LifterLMS add-ons. + * $ wp llms addon deactivate --all + * + * # Deactivate and uninstall the LifterLMS Groups add-on. + * $ wp llms addon deactivate lifterlms-groups --uninstall + * + * @since 0.0.1 + * @since 0.0.2 Completion messages use says "deactivate(d)" in favor of "activate(d)". + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function deactivate( $args, $assoc_args ) { + + if ( ! empty( $assoc_args['all'] ) ) { + $args = $this->get_available_addons( 'active', false, 'plugin' ); + if ( empty( $args ) ) { + return \WP_CLI::warning( 'No add-ons to deactivate.' ); + } + } + + $results = $this->loop( $args, $assoc_args, 'deactivate_one' ); + if ( ! $this->chaining ) { + \WP_CLI\Utils\report_batch_operation_results( 'add-on', 'deactivate', count( $args ), $results['successes'], $results['errors'] ); + } + + } + + /** + * Loop callback function for deactivate() + * + * Ensures add-on can be deactivated and actually deactivates (and maybe uninstalls) the add-on. + * + * @since 0.0.1 + * + * @param string $slug Add-on slug. + * @param LLMS_Add_On $addon Add-on object. + * @param array $assoc_args Associative array of command options. + * @return null|true Returns `null` if an error is encountered and `true` on success. + */ + private function deactivate_one( $slug, $addon, $assoc_args ) { + + if ( ! $addon->is_installed() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%1$s" is not installed.', $slug ) ); + } + + if ( ! $addon->is_active() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%s" is already deactivated.', $slug ) ); + } + + $res = $addon->deactivate(); + if ( is_wp_error( $res ) ) { + return \WP_CLI::warning( $res ); + } + + if ( ! empty( $assoc_args['uninstall'] ) ) { + $this->chain_command( 'uninstall', array( $slug ) ); + } + + \WP_CLI::log( $res ); + + return true; + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Enumerate.php b/libraries/lifterlms-cli/src/Commands/AddOn/Enumerate.php new file mode 100644 index 0000000000..d98d9eed07 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Enumerate.php @@ -0,0 +1,127 @@ +<?php +/** + * Addon List class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +use WP_CLI\Formatter; + +/** + * AddOn List command + * + * "List" is a php reserved keyword, so we enumerate instead. + * + * @since 0.0.1 + * + * @link https://www.php.net/manual/en/reserved.keywords.php + */ +trait Enumerate { + + /** + * Gets a list of add-ons. + * + * Displays a list of add-ons with their activation status, + * license status, current version, update availability, etc... + * + * ## OPTIONS + * + * [--<field>=<value>] + * : Filter results based on the value of a field. + * + * [--field=<field>] + * : Prints the value of a single field for each add-on. + * + * [--fields=<fields>] + * : Limit the output to only the specified fields. Use "all" to display all available fields. + * + * [--format=<format>] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - count + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each add-on: + * + * * name + * * status + * * update + * * version + * + * These fields are optionally available: + * + * * update_version + * * license + * * title + * * channel + * * type + * * file + * + * ## EXAMPLES + * + * # List all add-ons. + * $ wp llms addon list + * + * # List all add-ons in JSON format. + * $ wp llms addon list --format=json + * + * # List all add-ons by name only. + * $ wp llms addon list --field=name + * + * # List all add-ons with all available fields. + * $ wp llms addon list --fields=all + * + * # List all add-ons with a custom fields list. + * $ wp llms addon list --fields=title,status,version + * + * # List currently activated add-ons. + * $ wp llms addon list --status=active + * + * # List all theme add-ons. + * $ wp llms addon list --type=theme + * + * # List all add-ons with available updates. + * $ wp llms addon list --update=available + * + * # List all add-ons licensed on the site. + * $ wp llms addon list --license=active + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function list( $args, $assoc_args ) { + + $fields = array( 'name', 'status', 'update', 'version' ); + $all_fields = array_merge( $fields, array( 'update_version', 'license', 'title', 'channel', 'type', 'file' ) ); + + // Determine if there's a user filter submitted through`--<field>=<value>`. + $filter_field = array_values( array_intersect( $all_fields, array_keys( $assoc_args ) ) ); + + $list = $this->get_filtered_items( $assoc_args, ! empty( $filter_field ) ? $filter_field[0] : '' ); + + if ( ! empty( $assoc_args['fields'] ) && 'all' === $assoc_args['fields'] ) { + $assoc_args['fields'] = $all_fields; + } + + $formatter = new Formatter( $assoc_args, $fields ); + return $formatter->display_items( $list ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Get.php b/libraries/lifterlms-cli/src/Commands/AddOn/Get.php new file mode 100644 index 0000000000..0ea53099ee --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Get.php @@ -0,0 +1,120 @@ +<?php +/** + * Addon Get class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +use WP_CLI\Formatter; + +/** + * AddOn Get command + * + * @since 0.0.1 + */ +trait Get { + + /** + * Get information about an add-on. + * + * ## OPTIONS + * + * <slug> + * : The slug of the add-on to get information about. + * + * ## OPTIONS + * + * [--field=<field>] + * : Retrieve a single piece of information about the add-on. + * + * [--fields=<fields>] + * : Limit the output to only the specified fields. Use "all" to display all available fields. + * + * [--format=<format>] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## AVAILABLE FIELDS + * + * These fields will be displayed by default for each add-on: + * + * * name + * * title + * * version + * * description + * * status + * + * These fields are optionally available: + * + * * update + * * update_version + * * license + * * title + * * channel + * * type + * * file + * * permalink + * * changelog + * * documentation + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function get( $args, $assoc_args ) { + + $addon = $this->get_addon( $args[0], true ); + $fields = array( 'name', 'title', 'version', 'description', 'status' ); + $all_fields = array_merge( $fields, array( 'update', 'update_version', 'license', 'title', 'channel', 'type', 'file', 'permalink', 'changelog', 'documentation' ) ); + + if ( ! empty( $assoc_args['fields'] ) ) { + $assoc_args['fields'] = 'all' === $assoc_args['fields'] ? $all_fields : $assoc_args['fields']; + } else { + $assoc_args['fields'] = $fields; + } + + // Get formatted item. + $item = $this->format_item( $addon ); + + // Put the keys in the order defined by input args. + $item = array_merge( array_flip( $assoc_args['fields'] ), $item ); + + // Pass the item as an array and all fields for proper formatting when --field=<field> is passed. + $list = array( $item ); + $format_fields = $all_fields; + + // Format when displaying multiple fields. + if ( empty( $assoc_args['field'] ) ) { + + $list = array(); + foreach ( $item as $Field => $Value ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + if ( ! in_array( $Field, $assoc_args['fields'], true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + continue; + } + $list[] = compact( 'Field', 'Value' ); + } + $format_fields = array( 'Field', 'Value' ); + unset( $assoc_args['fields'] ); + + } + + $formatter = new Formatter( $assoc_args, $format_fields ); + return $formatter->display_items( $list ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Install.php b/libraries/lifterlms-cli/src/Commands/AddOn/Install.php new file mode 100644 index 0000000000..91892b1df0 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Install.php @@ -0,0 +1,106 @@ +<?php +/** + * Addon Install class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +/** + * AddOn Installation command + * + * @since 0.0.1 + */ +trait Install { + + /** + * Install one of more add-ons. + * + * ## OPTIONS + * + * [<slug>...] + * : The slug of one or more add-on to install. + * + * [--key=<key>] + * : If set, will attempt to activate and use the provided license key. + * + * [--activate] + * : If set, the add-on(s) will be activated immediately after install. + * + * [--all] + * : If set, all of the add-ons available to the site will be installed. + * All existing license keys stored on the site will be queried for the list of available add-ons. + * + * [--type=<type>] + * : When using '--all', determines the type of add-on to be installed. + * --- + * default: 'all' + * options: + * - all + * - plugin + * - theme + * --- + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function install( $args, $assoc_args ) { + + // If a key is provided, activate it first. + if ( ! empty( $assoc_args['key'] ) ) { + \WP_CLI::runcommand( "llms license activate {$assoc_args['key']}" ); + } + + if ( ! empty( $assoc_args['all'] ) ) { + $args = $this->get_available_addons( 'uninstalled', true, $assoc_args['type'] ); + if ( empty( $args ) ) { + return \WP_CLI::warning( 'No add-ons to install.' ); + } + } + + $results = $this->loop( $args, $assoc_args, 'install_one' ); + \WP_CLI\Utils\report_batch_operation_results( 'add-on', 'install', count( $args ), $results['successes'], $results['errors'] ); + + } + + /** + * Loop callback function for install() + * + * Ensures add-on can be installed and actually installs (and maybe activates) the add-on. + * + * @since 0.0.1 + * + * @param string $slug Add-on slug. + * @param LLMS_Add_On $addon Add-on object. + * @param array $assoc_args Associative array of command options. + * @return null|true Returns `null` if an error is encountered and `true` on success. + */ + private function install_one( $slug, $addon, $assoc_args ) { + + if ( $addon->is_installed() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%s" is already installed.', $slug ) ); + } + + \WP_CLI::log( sprintf( 'Installing add-on: %s...', $addon->get( 'title' ) ) ); + $res = $addon->install(); + if ( is_wp_error( $res ) ) { + return \WP_CLI::warning( $res ); + } + + \WP_CLI::log( $res ); + if ( ! empty( $assoc_args['activate'] ) ) { + $this->chain_command( 'activate', array( $slug ) ); + } + + return true; + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Main.php b/libraries/lifterlms-cli/src/Commands/AddOn/Main.php new file mode 100644 index 0000000000..b5beab0b95 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Main.php @@ -0,0 +1,202 @@ +<?php +/** + * LLMS_CLI_Command_Add_On file. + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +use LifterLMS\CLI\Commands\AbstractCommand; +use WP_CLI\Formatter; + +/** + * Manage LifterLMS add-on plugins and themes. + * + * @since 0.0.1 + */ +class Main extends AbstractCommand { + + // Include subcommands. + use Activate, + ChannelSet, + Deactivate, + Enumerate, + Get, + Install, + Uninstall, + Update; + + /** + * Accepts an add-on array and converts it to the format used by the output method + * + * @since 0.0.1 + * + * @param array|LLMS_Add_On $item_or_addon Add-on object or add-on item array array from `llms_get_add_ons()`. + * @return array Associative array containing all possible fields as used by the output method. + */ + private function format_item( $item_or_addon ) { + + $addon = is_array( $item_or_addon ) ? llms_get_add_on( $item_or_addon ) : $item_or_addon; + + $formatted = array( + 'name' => $addon->get( 'slug' ), + 'description' => $addon->get( 'description' ), + 'status' => $addon->get_status(), + 'license' => str_replace( 'license_', '', $addon->get_license_status() ), + 'update' => $addon->has_available_update() ? 'available' : 'none', + 'version' => $addon->is_installed() ? $addon->get_installed_version() : 'N/A', + 'update_version' => $addon->get( 'version' ), + 'title' => $addon->get( 'title' ), + 'channel' => $addon->get_channel_subscription(), + 'type' => $addon->get( 'type' ), + 'file' => $addon->get( 'update_file' ), + 'permalink' => $addon->get( 'permalink' ), + 'changelog' => $addon->get( 'changelog' ), + 'documentation' => $addon->get( 'documentation' ), + ); + + return $formatted; + } + + /** + * Retrieve an array of available add-on slugs based on the supplied query criteria. + * + * This function passes data to `wp llms addon list` with specific filters and returns an associative + * array of add-on slugs from that list. + * + * This is used, mostly, to generate a list of available addons for various commands which provide an `--all` flag/option. + * + * @since 0.0.1 + * + * @param string $status Add-on status, passed as the `--status` option to `llms addon list`. + * @param bool $check_license Whether or not the add-on should be licensed. This is used to determine what is installable / upgradeable. + * @param string $type Add-on type. Accepts 'all' (default), 'plugin' or 'theme'. + * @return string[] Array of add-on slugs meeting the specified filters. + */ + private function get_available_addons( $status, $check_license, $type = 'all' ) { + + $list = \WP_CLI::runcommand( + "llms addon list --format=json --status={$status} --fields=name,license,type", + array( + 'return' => true, + ) + ); + $list = array_filter( + json_decode( $list, true ), + function( $item ) use ( $check_license, $type ) { + return ( ( $check_license && 'active' === $item['license'] ) || ! $check_license ) && ( 'all' === $type || $type === $item['type'] ); + } + ); + + return wp_list_pluck( $list, 'name' ); + + } + + /** + * Retrieves an optionally filtered list of add-ons for use in the `list` command. + * + * @since 0.0.1 + * + * @param array $assoc_args Associative array of command options. + * @param string $filter_field The optional name of the field to filter results by. + * @return array[] Array of add-on items. + */ + private function get_filtered_items( $assoc_args, $filter_field = '' ) { + + $addons = llms_get_add_ons(); + + $list = array_filter( + $addons['items'], + function( $item ) { + return // Skip anything without a slug. + ! empty( $item['slug'] ) && + // Skip the LifterLMS core. + 'lifterlms' !== $item['slug'] && + // Skip third party add-ons. + ! in_array( 'third-party', array_keys( $item['categories'] ), true ); + } + ); + + // Format remaining items. + $list = array_map( array( $this, 'format_item' ), $list ); + + // Filter by field value. + if ( $filter_field ) { + $field_val = $assoc_args[ $filter_field ]; + $list = array_filter( + $list, + function( $item ) use ( $filter_field, $field_val ) { + return $item[ $filter_field ] === $field_val; + } + ); + } + + // Alpha sort the list by slug. + usort( + $list, + function( $a, $b ) { + return strcmp( $a['name'], $b['name'] ); + } + ); + + return $list; + + } + + /** + * Reusable loop function for handling commands which accept one or more slugs as the commands first argument + * + * @since 0.0.1 + * + * @param string[] $slugs Array of add-on slugs, with or without the `lifterlms-` prefix. + * @param array $assoc_args Associative array of command options from the original command. + * @param string $callback Name of the method to use for handling a single add-on for the given command. + * The callback should accept three arguments: + * + @type string $slug Add-on slug for the current item. + * + @type LLMS_Add_On $addon Add-on object for the current item. + * + @type array $assoc_args Array of arguments from the initial command. + * The callback should return a truthy to signal success and + * a falsy to signal an error. + * @return array { + * Associative arrays containing details on the errors and successes encountered during the loop. + * + * @type int $errors Number of errors encountered in the loop. + * @type int $successes Number of success encountered in the loop. + * } + */ + private function loop( $slugs, $assoc_args, $callback ) { + + $successes = 0; + $errors = 0; + + foreach ( $slugs as $slug ) { + + if ( empty( $slug ) ) { + \WP_CLI::warning( 'Ignoring ambiguous empty slug value.' ); + continue; + } + + $addon = $this->get_addon( $slug, true, 'warning' ); + if ( empty( $addon ) ) { + $errors++; + continue; + } + + if ( ! $this->$callback( $slug, $addon, $assoc_args ) ) { + $errors++; + continue; + } + + $successes++; + + } + + return compact( 'errors', 'successes' ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Uninstall.php b/libraries/lifterlms-cli/src/Commands/AddOn/Uninstall.php new file mode 100644 index 0000000000..101a92202e --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Uninstall.php @@ -0,0 +1,104 @@ +<?php +/** + * Addon Uninstall class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +/** + * AddOn Uninstall command + * + * @since 0.0.1 + */ +trait Uninstall { + + /** + * Uninstall one of more add-ons. + * + * ## OPTIONS + * + * [<slug>...] + * : The slug of one or more add-on to install. + * + * [--deactivate] + * : If set, the plugin add-on(s) will be deactivated prior to uninstalling. Default behavior is to warn and skip if the plugin is active. + * Themes cannot be deactivated, another theme must be activated and then an add-on theme can be uninstalled. + * + * [--all] + * : If set, all of the add-ons available to the site will be uninstalled. + * + * [--type=<type>] + * : When using '--all', determines the type of add-on to be uninstalled. + * --- + * default: 'all' + * options: + * - all + * - plugin + * - theme + * --- + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function uninstall( $args, $assoc_args ) { + + if ( ! empty( $assoc_args['all'] ) ) { + $args = $this->get_available_addons( 'inactive', false, $assoc_args['type'] ); + if ( empty( $args ) ) { + return \WP_CLI::warning( 'No add-ons to uninstall.' ); + } + } + + $results = $this->loop( $args, $assoc_args, 'uninstall_one' ); + if ( ! $this->chaining ) { + \WP_CLI\Utils\report_batch_operation_results( 'add-on', 'uninstall', count( $args ), $results['successes'], $results['errors'] ); + } + + } + + /** + * Loop callback function for uninstall() + * + * Ensures add-on can be uninstalled and actually installs (and maybe deactivates) the add-on. + * + * @since 0.0.1 + * + * @param string $slug Add-on slug. + * @param LLMS_Add_On $addon Add-on object. + * @param array $assoc_args Associative array of command options. + * @return null|true Returns `null` if an error is encountered and `true` on success. + */ + private function uninstall_one( $slug, $addon, $assoc_args ) { + + if ( ! $addon->is_installed() ) { + return \WP_CLI::warning( sprintf( 'Add-on "%s" is not installed.', $slug ) ); + } + + if ( $addon->is_active() ) { + if ( ! empty( $assoc_args['deactivate'] ) ) { + $this->chain_command( 'deactivate', array( $slug ) ); + } else { + return \WP_CLI::warning( sprintf( 'Add-on "%s" is active.', $slug ) ); + } + } + + $res = $addon->uninstall(); + if ( is_wp_error( $res ) ) { + return \WP_CLI::warning( $res ); + } + + \WP_CLI::log( $res ); + + return true; + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/AddOn/Update.php b/libraries/lifterlms-cli/src/Commands/AddOn/Update.php new file mode 100644 index 0000000000..b179dbdcee --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/AddOn/Update.php @@ -0,0 +1,165 @@ +<?php +/** + * Addon Update class file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI\Commands\AddOn; + +use WP_CLI\Formatter; + +/** + * AddOn Update command + * + * @since 0.0.1 + */ +trait Update { + + /** + * Update one of more add-ons. + * + * ## OPTIONS + * + * [<slug>...] + * : The slug of one or more add-on to update. + * + * [--exclude] + * : A comma-separated list of add-on slugs which should be excluded from updating. + * + * [--all] + * : If set, all of the add-ons available to the site will be uninstalled. + * + * [--type=<type>] + * : When using '--all', determines the type of add-on to be uninstalled. + * --- + * default: 'all' + * options: + * - all + * - plugin + * - theme + * --- + * + * [--format=<format>] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * [--dry-run] + * : Preview which plugins would be updated. + * + * @since 0.0.1 + * + * @param array $include List of add-on slugs to be updated. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function update( $include, $assoc_args ) { + + $include = array_map( array( $this, 'prefix_slug' ), $include ); + + $fields = array( 'name', 'status', 'version', 'update_version' ); + + $exclude = ! empty( $assoc_args['exclude'] ) ? array_map( array( $this, 'prefix_slug' ), explode( ',', $assoc_args['exclude'] ) ) : array(); + + // Retrieve all available updates and we'll filter it down. + $list = \WP_CLI::runcommand( + "llms addon list --format=json {$fieldopt}--update=available --fields=name,status,version,update_version", + array( + 'return' => true, + ) + ); + $list = array_filter( + json_decode( $list, true ), + function( $item ) use ( $include, $exclude ) { + // Add-on is active and an update is available. + return // Add-on is installed. + in_array( $item['status'], array( 'active', 'inactive' ), true ) && + // Not excluded. + ! in_array( $item['name'], $exclude, true ) && + // No add-ons specified or the add-on is in the specified list. + ( empty( $include ) || in_array( $item['name'], $include, true ) ); + } + ); + + // WP-CLI `wp plugin update` shows a string when displaying table and no output for other formats. + if ( empty( $list ) ) { + if ( 'table' === $assoc_args['format'] ) { + return \WP_CLI::log( 'No add-on updates available.' ); + } + return; + } + + /** + * The WP Core upgrader pulls information from the site transient. + * If the update check cron or a manual visit to an update screen on the admin panel + * hasn't recently occurred the transient won't be set and we'll know there's an update + * but the transient will not and the upgrader won't be able to upgrade. + * + * So we'll force a redundant check to take place here to ensure that we can upgrade. + */ + wp_update_plugins(); + wp_update_themes(); + + if ( empty( $assoc_args['dry-run'] ) ) { + + $fields = array( 'name', 'status', 'old_version', 'new_version' ); + + $errors = 0; + $successes = 0; + foreach ( $list as &$item ) { + + if ( $this->update_one( $item ) ) { + $successes++; + } else { + $errors++; + } + } + + \WP_CLI\Utils\report_batch_operation_results( 'add-on', 'update', count( $list ), $successes, $errors ); + + } + + $formatter = new Formatter( $assoc_args, $fields ); + return $formatter->display_items( $list ); + + } + + + /** + * Update a single add-on + * + * @since 0.0.1 + * + * @param array $item Associative array of add-on data. + * @return boolean Returns `false` when an error is encountered and `true` otherwise. + */ + private function update_one( &$item ) { + + $addon = $this->get_addon( $item['name'] ); + + \WP_CLI::log( sprintf( 'Updating add-on: %s...', $addon->get( 'title' ) ) ); + $res = $addon->update(); + if ( is_wp_error( $res ) ) { + \WP_CLI::warning( $res ); + return false; + } + + $item['old_version'] = $item['version']; + $item['new_version'] = $item['update_version']; + + \WP_CLI::log( $res ); + return true; + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/License.php b/libraries/lifterlms-cli/src/Commands/License.php new file mode 100644 index 0000000000..650ec36dc9 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/License.php @@ -0,0 +1,105 @@ +<?php +/** + * License command file + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.3 + */ + +namespace LifterLMS\CLI\Commands; + +use WP_CLI\Formatter; + +/** + * Manage LifterLMS License Keys. + * + * @since 0.0.1 + */ +class License extends AbstractCommand { + + /** + * Activate a license key. + * + * ## OPTIONS + * + * [<key>] + * : The license key to be activated. + * + * @since 0.0.1 + * + * @param array $args Indexed array of positional command arguments. + * @return null + */ + public function activate( $args ) { + + $res = \LLMS_Helper_Keys::activate_keys( $args[0] ); + if ( ! empty( $res['data']['errors'] ) ) { + return \WP_CLI::error( $res['data']['errors'][0] ); + } elseif ( ! empty( $res['data']['activations'] ) ) { + \LLMS_Helper_Keys::add_license_key( $res['data']['activations'][0] ); + return \WP_CLI::success( sprintf( 'License key "%s" has been activated on this site.', $args[0] ) ); + } + + return \WP_CLI::error( 'An unknown error was encountered.' ); + + } + + /** + * Deactivate a license key. + * + * ## OPTIONS + * + * [<key>] + * : The license key to be deactivated. + * + * @since 0.0.1 + * @since 0.0.2 Use a strict comparison when checking response status. + * + * @param array $args Indexed array of positional command arguments. + * @return null + */ + public function deactivate( $args ) { + + $res = \LLMS_Helper_Keys::deactivate_keys( array( $args[0] ) ); + if ( ! empty( $res['data']['errors'] ) ) { + return \WP_CLI::error( $res['data']['errors'][0] ); + } elseif ( ! empty( $res['data']['deactivations'] ) ) { + \LLMS_Helper_Keys::remove_license_key( $args[0] ); + return \WP_CLI::success( sprintf( 'License key "%s" has been deactivated from this site.', $args[0] ) ); + } elseif ( ! empty( $res['data']['status'] ) && 200 === absint( $res['data']['status'] ) ) { + return \WP_CLI::error( sprintf( 'License key "%s" was not active on this site.', $args[0] ) ); + } + + return \WP_CLI::error( 'An unknown error was encountered.' ); + + } + + /** + * List activated license keys. + * + * ## OPTIONS + * + * [<key>] + * : The license key to be deactivated. + * + * @since 0.0.1 + * + * @return null + */ + public function list() { + + $list = array_keys( llms_helper_options()->get_license_keys() ); + + if ( 0 === count( $list ) ) { + return \WP_CLI::warning( 'No license keys found on this site.' ); + } + + foreach ( $list as $key ) { + \WP_CLI::log( $key ); + } + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/Restful/Command.php b/libraries/lifterlms-cli/src/Commands/Restful/Command.php new file mode 100644 index 0000000000..7204a6d949 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/Restful/Command.php @@ -0,0 +1,670 @@ +<?php +/** + * LifterLMS CLI Restful Command file + * + * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT). + * https://github.com/wp-cli/restful + * + * @package LifterLMS_CLI/Classes + * + * @since 0.0.1 + * @version 0.0.1 + * + * @link https://github.com/wp-cli/restful/blob/master/inc/RestCommand.php + * @link https://github.com/wp-cli/restful/commit/021f1731c737fc1cb36ee06f0c34b73eb0d6aabb + */ + +namespace LifterLMS\CLI\Commands\Restful; + +/** + * LifterLMS CLI Restful Commands + * + * @since 0.0.1 + */ +class Command { + + private $scope = 'internal'; + private $api_url = ''; + private $auth = array(); + private $name; + private $route; + private $resource_identifier; + private $schema; + private $default_context = ''; + private $output_nesting_level = 0; + + public function __construct( $name, $route, $schema ) { + $this->name = $name; + $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); + $this->resource_identifier = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; + $this->route = rtrim( $route ); + $this->schema = $schema; + } + + /** + * Create a new item. + * + * @subcommand create + */ + public function create_item( $args, $assoc_args ) { + list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + \WP_CLI::line( $body['id'] ); + } else { + \WP_CLI::success( "Created {$this->name} {$body['id']}." ); + } + } + + /** + * Generate some items. + * + * @subcommand generate + */ + public function generate_items( $args, $assoc_args ) { + + $count = $assoc_args['count']; + unset( $assoc_args['count'] ); + $format = $assoc_args['format']; + unset( $assoc_args['format'] ); + + $notify = false; + if ( 'progress' === $format ) { + $notify = \WP_CLI\Utils\make_progress_bar( 'Generating items', $count ); + } + + for ( $i = 0; $i < $count; $i++ ) { + + list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); + + if ( 'progress' === $format ) { + $notify->tick(); + } elseif ( 'ids' === $format ) { + echo $body['id']; + if ( $i < $count - 1 ) { + echo ' '; + } + } + } + + if ( 'progress' === $format ) { + $notify->finish(); + } + } + + /** + * Delete an existing item. + * + * @subcommand delete + */ + public function delete_item( $args, $assoc_args ) { + list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); + $id = isset( $body['previous'] ) ? $body['previous']['id'] : $body['id']; + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + \WP_CLI::line( $id ); + } else { + if ( empty( $assoc_args['force'] ) ) { + \WP_CLI::success( "Trashed {$this->name} {$id}." ); + } else { + \WP_CLI::success( "Deleted {$this->name} {$id}." ); + } + } + } + + /** + * Get a single item. + * + * @subcommand get + */ + public function get_item( $args, $assoc_args ) { + list( $status, $body, $headers ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); + + if ( ! empty( $assoc_args['fields'] ) ) { + $body = self::limit_item_to_fields( $body, $fields ); + } + + if ( 'headers' === $assoc_args['format'] ) { + echo json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo json_encode( + array( + 'body' => $body, + 'headers' => $headers, + 'status' => $status, + 'api_url' => $this->api_url, + ) + ); + } else { + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_item( $body ); + } + } + + /** + * List all items. + * + * @subcommand list + */ + public function list_items( $args, $assoc_args ) { + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + $method = 'HEAD'; + } else { + $method = 'GET'; + } + list( $status, $body, $headers ) = $this->do_request( $method, $this->get_base_route(), $assoc_args ); + if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { + $items = array_column( $body, 'id' ); + } else { + $items = $body; + } + + if ( ! empty( $assoc_args['fields'] ) ) { + foreach ( $items as $key => $item ) { + $items[ $key ] = self::limit_item_to_fields( $item, $fields ); + } + } + + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + echo (int) $headers['X-WP-Total']; + } elseif ( 'headers' === $assoc_args['format'] ) { + echo json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo json_encode( + array( + 'body' => $body, + 'headers' => $headers, + 'status' => $status, + 'api_url' => $this->api_url, + ) + ); + } else { + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_items( $items ); + } + } + + /** + * Compare items between environments. + * + * <alias> + * : Alias for the WordPress site to compare to. + * + * [<resource>] + * : Limit comparison to a specific resource, instead of the collection. + * + * [--fields=<fields>] + * : Limit comparison to specific fields. + * + * @subcommand diff + */ + public function diff_items( $args, $assoc_args ) { + + list( $alias ) = $args; + if ( ! array_key_exists( $alias, \WP_CLI::get_runner()->aliases ) ) { + \WP_CLI::error( "Alias '{$alias}' not found." ); + } + $resource = isset( $args[1] ) ? $args[1] : null; + $fields = \WP_CLI\Utils\get_flag_value( $assoc_args, 'fields', null ); + + list( $from_status, $from_body, $from_headers ) = $this->do_request( 'GET', $this->get_base_route(), array() ); + + $php_bin = \WP_CLI::get_php_binary(); + $script_path = $GLOBALS['argv'][0]; + $other_args = implode( ' ', array_map( 'escapeshellarg', array( $alias, 'rest', $this->name, 'list' ) ) ); + $other_assoc_args = \WP_CLI\Utils\assoc_args_to_str( array( 'format' => 'envelope' ) ); + $full_command = "{$php_bin} {$script_path} {$other_args} {$other_assoc_args}"; + $process = \WP_CLI\Process::create( + $full_command, + null, + array( + 'HOME' => getenv( 'HOME' ), + 'WP_CLI_PACKAGES_DIR' => getenv( 'WP_CLI_PACKAGES_DIR' ), + 'WP_CLI_CONFIG_PATH' => getenv( 'WP_CLI_CONFIG_PATH' ), + ) + ); + $result = $process->run(); + $response = json_decode( $result->stdout, true ); + $to_headers = $response['headers']; + $to_body = $response['body']; + $to_api_url = $response['api_url']; + + if ( ! is_null( $resource ) ) { + $field = is_numeric( $resource ) ? 'id' : 'slug'; + $callback = function( $value ) use ( $field, $resource ) { + if ( isset( $value[ $field ] ) && $resource == $value[ $field ] ) { + return true; + } + return false; + }; + foreach ( array( 'to_body', 'from_body' ) as $response_type ) { + $$response_type = array_filter( $$response_type, $callback ); + } + } + + $display_items = array(); + do { + $from_item = $to_item = array(); + if ( ! empty( $from_body ) ) { + $from_item = array_shift( $from_body ); + if ( ! empty( $to_body ) && ! empty( $from_item['slug'] ) ) { + foreach ( $to_body as $i => $item ) { + if ( ! empty( $item['slug'] ) && $item['slug'] === $from_item['slug'] ) { + $to_item = $item; + unset( $to_body[ $i ] ); + break; + } + } + } + } elseif ( ! empty( $to_body ) ) { + $to_item = array_shift( $to_body ); + } + + if ( ! empty( $to_item ) ) { + foreach ( array( 'to_item', 'from_item' ) as $item ) { + if ( isset( $$item['_links'] ) ) { + unset( $$item['_links'] ); + } + } + $display_items[] = array( + 'from' => self::limit_item_to_fields( $from_item, $fields ), + 'to' => self::limit_item_to_fields( $to_item, $fields ), + ); + } + } while ( count( $from_body ) || count( $to_body ) ); + + \WP_CLI::line( \cli\Colors::colorize( "%R(-) {$this->api_url} %G(+) {$to_api_url}%n" ) ); + foreach ( $display_items as $display_item ) { + $this->show_difference( + $this->name, + array( + 'from' => $display_item['from'], + 'to' => $display_item['to'], + ) + ); + } + } + + /** + * Update an existing item. + * + * @subcommand update + */ + public function update_item( $args, $assoc_args ) { + list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + \WP_CLI::line( $body['id'] ); + } else { + \WP_CLI::success( "Updated {$this->name} {$body['id']}." ); + } + } + + /** + * Open an existing item in the editor + * + * @subcommand edit + */ + public function edit_item( $args, $assoc_args ) { + $assoc_args['context'] = 'edit'; + list( $status, $options_body ) = $this->do_request( 'OPTIONS', $this->get_filled_route( $args ), $assoc_args ); + if ( empty( $options_body['schema'] ) ) { + \WP_CLI::error( 'Cannot edit - no schema found for resource.' ); + } + $schema = $options_body['schema']; + list( $status, $resource_fields ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); + $editable_fields = array(); + foreach ( $resource_fields as $key => $value ) { + if ( ! isset( $schema['properties'][ $key ] ) || ! empty( $schema['properties'][ $key ]['readonly'] ) ) { + continue; + } + $properties = $schema['properties'][ $key ]; + if ( isset( $properties['properties'] ) ) { + $parent_key = $key; + $properties = $properties['properties']; + foreach ( $value as $key => $value ) { + if ( isset( $properties[ $key ] ) && empty( $properties[ $key ]['readonly'] ) ) { + if ( ! isset( $editable_fields[ $parent_key ] ) ) { + $editable_fields[ $parent_key ] = array(); + } + $editable_fields[ $parent_key ][ $key ] = $value; + } + } + continue; + } + if ( empty( $properties['readonly'] ) ) { + $editable_fields[ $key ] = $value; + } + } + if ( empty( $editable_fields ) ) { + \WP_CLI::error( 'Cannot edit - no editable fields found on schema.' ); + } + $ret = \WP_CLI\Utils\launch_editor_for_input( \Spyc::YAMLDump( $editable_fields ), sprintf( 'Editing %s %s', $schema['title'], $args[0] ) ); + if ( false === $ret ) { + \WP_CLI::warning( 'No edits made.' ); + } else { + list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), \Spyc::YAMLLoadString( $ret ) ); + \WP_CLI::success( "Updated {$schema['title']} {$args[0]}." ); + } + } + + /** + * Do a REST Request + * + * @param string $method + */ + private function do_request( $method, $route, $assoc_args ) { + if ( 'internal' === $this->scope ) { + if ( ! defined( 'REST_REQUEST' ) ) { + define( 'REST_REQUEST', true ); + } + $request = new \WP_REST_Request( $method, $route ); + if ( in_array( $method, array( 'POST', 'PUT' ) ) ) { + $request->set_body_params( $assoc_args ); + } else { + foreach ( $assoc_args as $key => $value ) { + $request->set_param( $key, $value ); + } + } + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); + } + $response = rest_do_request( $request ); + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $performed_queries = array(); + foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { + if ( in_array( $key, $original_queries ) ) { + continue; + } + $performed_queries[] = $query; + } + usort( + $performed_queries, + function( $a, $b ) { + if ( $a[1] === $b[1] ) { + return 0; + } + return ( $a[1] > $b[1] ) ? -1 : 1; + } + ); + + $query_count = count( $performed_queries ); + $query_total_time = 0; + foreach ( $performed_queries as $query ) { + $query_total_time += $query[1]; + } + $slow_query_message = ''; + if ( $performed_queries && 'rest' === \WP_CLI::get_config( 'debug' ) ) { + $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; + foreach ( $performed_queries as $i => $query ) { + $i++; + $bits = explode( ', ', $query[2] ); + $backtrace = implode( ', ', array_slice( $bits, 13 ) ); + $seconds = round( $query[1], 6 ); + $slow_query_message .= <<<EOT +{$i}: + - {$seconds} seconds + - {$backtrace} + - {$query[0]} +EOT; + $slow_query_message .= PHP_EOL; + } + } elseif ( 'rest' !== \WP_CLI::get_config( 'debug' ) ) { + $slow_query_message = '. Use --debug=rest to see all queries.'; + } + $query_total_time = round( $query_total_time, 6 ); + \WP_CLI::debug( "REST command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'rest' ); + } + if ( $error = $response->as_error() ) { + \WP_CLI::error( $error ); + } + return array( $response->get_status(), $response->get_data(), $response->get_headers() ); + } elseif ( 'http' === $this->scope ) { + $headers = array(); + if ( ! empty( $this->auth ) && 'basic' === $this->auth['type'] ) { + $headers['Authorization'] = 'Basic ' . base64_encode( $this->auth['username'] . ':' . $this->auth['password'] ); + } + if ( 'OPTIONS' === $method ) { + $method = 'GET'; + $assoc_args['_method'] = 'OPTIONS'; + } + $response = \WP_CLI\Utils\http_request( $method, rtrim( $this->api_url, '/' ) . $route, $assoc_args, $headers ); + $body = json_decode( $response->body, true ); + if ( $response->status_code >= 400 ) { + if ( ! empty( $body['message'] ) ) { + \WP_CLI::error( $body['message'] . ' ' . json_encode( array( 'status' => $response->status_code ) ) ); + } else { + switch ( $response->status_code ) { + case 404: + \WP_CLI::error( "No {$this->name} found." ); + break; + default: + \WP_CLI::error( 'Could not complete request.' ); + break; + } + } + } + return array( $response->status_code, json_decode( $response->body, true ), $response->headers->getAll() ); + } + \WP_CLI::error( 'Invalid scope for REST command.' ); + } + + /** + * Get Formatter object based on supplied parameters. + * + * @param array $assoc_args Parameters passed to command. Determines formatting. + * @return \WP_CLI\Formatter + */ + protected function get_formatter( &$assoc_args ) { + if ( ! empty( $assoc_args['fields'] ) ) { + if ( is_string( $assoc_args['fields'] ) ) { + $fields = explode( ',', $assoc_args['fields'] ); + } else { + $fields = $assoc_args['fields']; + } + } else { + if ( ! empty( $assoc_args['context'] ) ) { + $fields = $this->get_context_fields( $assoc_args['context'] ); + } else { + $fields = $this->get_context_fields( 'view' ); + } + } + return new \WP_CLI\Formatter( $assoc_args, $fields ); + } + + /** + * Get a list of fields present in a given context + * + * @param string $context + * @return array + */ + private function get_context_fields( $context ) { + $fields = array(); + foreach ( $this->schema['properties'] as $key => $args ) { + if ( empty( $args['context'] ) || in_array( $context, $args['context'] ) ) { + $fields[] = $key; + } + } + return $fields; + } + + /** + * Get the base route for this resource + * + * @return string + */ + private function get_base_route() { + return substr( $this->route, 0, strlen( $this->route ) - strlen( $this->resource_identifier ) ); + } + + /** + * Fill the route based on provided $args + */ + private function get_filled_route( $args ) { + return rtrim( $this->get_base_route(), '/' ) . '/' . $args[0]; + } + + /** + * Visually depict the difference between "dictated" and "current" + * + * @param array + */ + private function show_difference( $slug, $difference ) { + $this->output_nesting_level = 0; + $this->nested_line( $slug . ': ' ); + $this->recursively_show_difference( $difference['to'], $difference['from'] ); + $this->output_nesting_level = 0; + } + + /** + * Recursively output the difference between "dictated" and "current" + */ + private function recursively_show_difference( $dictated, $current = null ) { + + $this->output_nesting_level++; + + if ( $this->is_assoc_array( $dictated ) ) { + + foreach ( $dictated as $key => $value ) { + + if ( $this->is_assoc_array( $value ) || is_array( $value ) ) { + + $new_current = isset( $current[ $key ] ) ? $current[ $key ] : null; + if ( $new_current ) { + $this->nested_line( $key . ': ' ); + } else { + $this->add_line( $key . ': ' ); + } + + $this->recursively_show_difference( $value, $new_current ); + + } elseif ( is_string( $value ) ) { + + $pre = $key . ': '; + + if ( isset( $current[ $key ] ) && $current[ $key ] !== $value ) { + + $this->remove_line( $pre . $current[ $key ] ); + $this->add_line( $pre . $value ); + + } elseif ( ! isset( $current[ $key ] ) ) { + + $this->add_line( $pre . $value ); + + } + } + } + } elseif ( is_array( $dictated ) ) { + + foreach ( $dictated as $value ) { + + if ( ! $current + || ! in_array( $value, $current ) ) { + $this->add_line( '- ' . $value ); + } + } + } elseif ( is_string( $value ) ) { + + $pre = $key . ': '; + + if ( isset( $current[ $key ] ) && $current[ $key ] !== $value ) { + + $this->remove_line( $pre . $current[ $key ] ); + $this->add_line( $pre . $value ); + + } elseif ( ! isset( $current[ $key ] ) ) { + + $this->add_line( $pre . $value ); + + } else { + + $this->nested_line( $pre ); + + } + } + + $this->output_nesting_level--; + + } + + /** + * Output a line to be added + * + * @param string + */ + private function add_line( $line ) { + $this->nested_line( $line, 'add' ); + } + + /** + * Output a line to be removed + * + * @param string + */ + private function remove_line( $line ) { + $this->nested_line( $line, 'remove' ); + } + + /** + * Output a line that's appropriately nested + */ + private function nested_line( $line, $change = false ) { + + if ( 'add' == $change ) { + $color = '%G'; + $label = '+ '; + } elseif ( 'remove' == $change ) { + $color = '%R'; + $label = '- '; + } else { + $color = false; + $label = false; + } + + $spaces = ( $this->output_nesting_level * 2 ) + 2; + if ( $color && $label ) { + $line = \cli\Colors::colorize( "{$color}{$label}" ) . $line . \cli\Colors::colorize( '%n' ); + $spaces = $spaces - 2; + } + \WP_CLI::line( str_pad( ' ', $spaces ) . $line ); + } + + /** + * Whether or not this is an associative array + * + * @param array + * @return bool + */ + private function is_assoc_array( $array ) { + + if ( ! is_array( $array ) ) { + return false; + } + + return array_keys( $array ) !== range( 0, count( $array ) - 1 ); + } + + /** + * Reduce an item to specific fields. + * + * @param array $item + * @param array $fields + * @return array + */ + private static function limit_item_to_fields( $item, $fields ) { + if ( empty( $fields ) ) { + return $item; + } + if ( is_string( $fields ) ) { + $fields = explode( ',', $fields ); + } + foreach ( $item as $i => $field ) { + if ( ! in_array( $i, $fields ) ) { + unset( $item[ $i ] ); + } + } + return $item; + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/Restful/Runner.php b/libraries/lifterlms-cli/src/Commands/Restful/Runner.php new file mode 100644 index 0000000000..a6d2a3f739 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/Restful/Runner.php @@ -0,0 +1,391 @@ +<?php +/** + * File Summary + * + * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT). + * https://github.com/wp-cli/restful + * + * @package LifterLMS_CLI/Classes + * + * @since 0.0.1 + * @version 0.0.1 + * + * @link https://github.com/wp-cli/restful/blob/master/inc/Runner.php + * @link https://github.com/wp-cli/restful/commit/6ea62c149944d8fcb31a7ade7b4f65fb72c8a5a3 + */ + +namespace LifterLMS\CLI\Commands\Restful; + +/** + * LifterLMS REST API to LifterLMS CLI Bridge. + * + * Hooks into the REST API, figures out which endpoints come from LifterLMS, + * and registers them as CLI commands. + * + * @since 0.0.1 + */ +class Runner { + + public static function after_wp_load() { + + if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) { + return; + } + + if ( ! class_exists( 'WP_REST_Server' ) ) { + return; + } + + global $wp_rest_server; + $wp_rest_server = new \WP_REST_Server(); + + do_action( 'rest_api_init', $wp_rest_server ); + + $request = new \WP_REST_Request( 'GET', '/' ); + $request->set_param( 'context', 'help' ); + + $response = $wp_rest_server->dispatch( $request ); + $response_data = $response->get_data(); + if ( empty( $response_data ) ) { + return; + } + + foreach ( $response_data['routes'] as $route => $route_data ) { + + // Skip non LifterLMS routes. + if ( 0 !== strpos( $route, '/llms/' ) ) { + continue; + } + + if ( empty( $route_data['schema']['title'] ) ) { + \WP_CLI::debug( "No schema title found for {$route}, skipping LifterLMS CLI REST command registration.", 'lifterlms' ); + continue; + } + + $name = $route_data['schema']['title']; + $rest_command = new Command( $name, $route, $route_data['schema'] ); + self::register_route_commands( $rest_command, $route, $route_data ); + + } + + } + + + private static function get_command_root_desc( $resource ) { + $resource = str_replace( array( '-', 'students', 'api' ), array( ' ', 'student', 'API' ), $resource ); + if ( 's' !== substr( $resource, -1 ) ) { + $resource .= 's'; + } + return sprintf( 'Manage %s.', $resource ); + } + + private static function get_command_short_desc( $command, $resource ) { + + $before = ''; + $after = ''; + + + switch ( $command ) { + case 'create': + $before = 'Creates a new'; + break; + + case 'delete': + $before = 'Deletes an existing'; + break; + + case 'diff': + $before = 'Compare'; + $resource = self::pluralize_resource( $resource ); + $after = 'between environments'; + break; + + case 'edit': + $before = 'Launches system editor to edit the'; + $after = 'content'; + break; + + case 'generate': + $before = 'Generates some'; + $resource = self::pluralize_resource( $resource ); + break; + + case 'get': + $before = 'Gets details about a'; + break; + + case 'list': + $before = 'Gets a list of '; + $resource = self::pluralize_resource( $resource ); + break; + + case 'update': + $before = 'Updates an existing'; + break; + } + + return trim( implode( ' ', array( $before, $resource, $after ) ) ) . '.'; + } + + private static function pluralize_resource( $resource ) { + + switch ( $resource ) { + default: + $resource .= 's'; + } + + return $resource; + } + + private static function get_supported_commands( $route, $route_data ) { + + $supported_commands = array(); + foreach ( $route_data['endpoints'] as $endpoint ) { + + $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); + $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; + $trimmed_route = rtrim( $route ); + $is_singular = $resource_id === substr( $trimmed_route, - strlen( $resource_id ) ); + + // List a collection + if ( array( 'GET' ) == $endpoint['methods'] + && ! $is_singular ) { + $supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + + // Create a specific resource + if ( array( 'POST' ) == $endpoint['methods'] + && ! $is_singular ) { + $supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + + // Get a specific resource + if ( array( 'GET' ) == $endpoint['methods'] + && $is_singular ) { + $supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + + // Update a specific resource + if ( in_array( 'POST', $endpoint['methods'] ) + && $is_singular ) { + $supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + + // Delete a specific resource + if ( array( 'DELETE' ) == $endpoint['methods'] + && $is_singular ) { + $supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + } + + return $supported_commands; + + } + + public static function before_invoke_command() { + + /** + * If `--user` was passed the user will already be set, otherwise there won't be a user. + * + * It is "safe" to assume that someone using the CLI has admin access and we'll set the current + * user to be the first admin we find in the DB that has the `manage_options` cap. + */ + if ( ! get_current_user_id() ) { + $user = \LLMS_Install::get_can_install_user_id(); + if ( $user ) { + wp_set_current_user( $user ); + } + } + + if ( \WP_CLI::get_config( 'debug' ) && ! defined( 'SAVEQUERIES' ) ) { + define( 'SAVEQUERIES', true ); + } + + } + + /** + * Register WP-CLI commands for all endpoints on a route + * + * @param string + * @param array $endpoints + */ + private static function register_route_commands( $rest_command, $route, $route_data ) { + + $resource = str_replace( array( 'llms_', '_' ), array( '', '-' ), $route_data['schema']['title'] ); + $parent = "llms {$resource}"; + + $supported_commands = self::get_supported_commands( $route, $route_data ); + foreach ( $supported_commands as $command => $endpoint_args ) { + + $synopsis = array(); + if ( in_array( $command, array( 'delete', 'get', 'update' ) ) ) { + $synopsis[] = array( + 'name' => 'id', + 'type' => 'positional', + 'description' => 'The id for the resource.', + 'optional' => false, + ); + } + + foreach ( $endpoint_args as $name => $args ) { + $arg_reg = array( + 'name' => $name, + 'type' => 'assoc', + 'description' => ! empty( $args['description'] ) ? $args['description'] : '', + 'optional' => empty( $args['required'] ) ? true : false, + ); + foreach ( array( 'enum', 'default' ) as $key ) { + if ( isset( $args[ $key ] ) ) { + $new_key = 'enum' === $key ? 'options' : $key; + $arg_reg[ $new_key ] = $args[ $key ]; + } + } + $synopsis[] = $arg_reg; + } + + if ( in_array( $command, array( 'list', 'get' ) ) ) { + $synopsis[] = array( + 'name' => 'fields', + 'type' => 'assoc', + 'description' => 'Limit response to specific fields. Defaults to all fields.', + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'field', + 'type' => 'assoc', + 'description' => 'Get the value of an individual field.', + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'format', + 'type' => 'assoc', + 'description' => 'Render response in a particular format.', + 'optional' => true, + 'default' => 'table', + 'options' => array( + 'table', + 'json', + 'csv', + 'ids', + 'yaml', + 'count', + 'headers', + 'body', + 'envelope', + ), + ); + } + + if ( in_array( $command, array( 'create', 'update', 'delete' ) ) ) { + $synopsis[] = array( + 'name' => 'porcelain', + 'type' => 'flag', + 'description' => 'Output just the id when the operation is successful.', + 'optional' => true, + ); + } + + $methods = array( + 'list' => 'list_items', + 'create' => 'create_item', + 'delete' => 'delete_item', + 'get' => 'get_item', + 'update' => 'update_item', + ); + + // Add the root command, eg: wp llms course. + \WP_CLI::add_command( + "{$parent}", + $rest_command, + array( + 'shortdesc' => self::get_command_root_desc( $resource ), + ) + ); + + // Register main subcommands, eg: wp llms course create, wp llms course delete, etc... + \WP_CLI::add_command( + "{$parent} {$command}", + array( $rest_command, $methods[ $command ] ), + array( + 'shortdesc' => self::get_command_short_desc( $command, $resource ), + 'synopsis' => $synopsis, + 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), + ) + ); + + // If listing is supported, add the diff command. + if ( 'list' === $command ) { + \WP_CLI::add_command( + "{$parent} diff", + array( $rest_command, 'diff_items' ), + array( + 'shortdesc' => self::get_command_short_desc( 'diff', $resource ), + 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), + ) + ); + } + + // If creation is supported, add the generate command. + if ( 'create' === $command ) { + \WP_CLI::add_command( + "{$parent} generate", + array( $rest_command, 'generate_items' ), + array( + 'shortdesc' => self::get_command_short_desc( 'generate', $resource ), + 'synopsis' => self::get_generate_command_synopsis( $synopsis ), + 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), + ) + ); + } + + + // If updating and getting is supported, add the edit command. + if ( 'update' === $command && array_key_exists( 'get', $supported_commands ) ) { + $synopsis = array(); + $synopsis[] = array( + 'name' => 'id', + 'type' => 'positional', + 'description' => 'The id for the resource.', + 'optional' => false, + ); + \WP_CLI::add_command( + "{$parent} edit", + array( $rest_command, 'edit_item' ), + array( + 'shortdesc' => self::get_command_short_desc( 'edit', $resource ), + 'synopsis' => $synopsis, + 'before_invoke' => array( __CLASS__, 'before_invoke_command' ), + ) + ); + } + } + } + + private static function get_generate_command_synopsis( $create_synopsis ) { + + $generate_synopsis = array( + array( + 'name' => 'count', + 'type' => 'assoc', + 'description' => 'Number of items to generate.', + 'optional' => true, + 'default' => 10, + ), + array( + 'name' => 'format', + 'type' => 'assoc', + 'description' => 'Render generation in specific format.', + 'optional' => true, + 'default' => 'progress', + 'options' => array( + 'progress', + 'ids', + ), + ), + ); + + return array_merge( $generate_synopsis, $create_synopsis ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Commands/Root.php b/libraries/lifterlms-cli/src/Commands/Root.php new file mode 100644 index 0000000000..0da54b4a48 --- /dev/null +++ b/libraries/lifterlms-cli/src/Commands/Root.php @@ -0,0 +1,83 @@ +<?php +/** + * LLMS_CLI_Command_Root file. + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.3 + */ + +namespace LifterLMS\CLI\Commands; + +/** + * Manage LifterLMS. + * + * ## BETA WARNING + * + * The LLMS-CLI is currently in early release as an open public beta. Commands + * are subject to change without warning. Please pay close attention to the + * changelog as we continue to develop and improve the CLI. + * + * 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. + * + * @since 0.0.1 + */ +class Root extends AbstractCommand { + + /** + * Display the version of LifterLMS or the specified LifterLMS add-on. + * + * ## OPTIONS + * + * [<slug>] + * : The slug of the LifterLMS plugin or theme. Default: lifterlms. + * + * ## EXAMPLES + * + * # Show the LifterLMS core plugin version + * wp llms version + * + * # Show the LifterLMS core plugin version + * wp llms version core + * + * # Show an add-on version without the "lifterlms-" prefix. + * wp llms version groups + * + * # Show an add-on version with the "lifterlms-" prefix. + * wp llms version lifterlms-assignments + * + * @since 0.0.1 + * @since 0.0.2 Remove `--db` option. This will be implemented in a separate command. + * + * @param array $args Indexed array of positional command arguments. + * @param array $assoc_args Associative array of command options. + * @return null + */ + public function version( $args, $assoc_args ) { + + $slug = empty( $args[0] ) ? 'core' : $args[0]; + if ( in_array( $slug, array( 'core', 'lifterlms' ), true ) ) { + return \WP_CLI::log( llms()->version ); + } + + $addon = $this->get_addon( $slug ); + if ( empty( $addon ) ) { + return \WP_CLI::error( 'Invalid slug.' ); + } + + if ( $addon->is_installed() ) { + return \WP_CLI::log( $addon->get_installed_version() ); + } + + return \WP_CLI::error( + sprintf( + "The requested add-on is not installed. Run 'wp llms addon install %s.' to install it.", + $args[0] + ) + ); + + } + +} diff --git a/libraries/lifterlms-cli/src/Main.php b/libraries/lifterlms-cli/src/Main.php new file mode 100644 index 0000000000..4cd19b55e3 --- /dev/null +++ b/libraries/lifterlms-cli/src/Main.php @@ -0,0 +1,140 @@ +<?php +/** + * LifterLMS CLI Main Class file + * + * @package LifterLMS_CLI/Classes + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI; + +use WP_CLI\Dispatcher\CommandAddition; + +defined( 'ABSPATH' ) || exit; + +/** + * LifterLMS Assignments Main Class + * + * @since 0.0.1 + */ +final class Main { + + /** + * Current version of the plugin + * + * @var string + */ + public $version = '0.0.1'; + + /** + * Singleton instance of the class + * + * @var LifterLMS_CLI + */ + private static $instance = null; + + /** + * Singleton Instance of the LifterLMS_CLI class + * + * @since 0.0.1 + * + * @return LifterLMS_CLI + */ + public static function instance() { + + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + + return self::$instance; + + } + + /** + * Constructor + * + * @since 0.0.1 + * + * @return void + */ + private function __construct() { + + if ( ! defined( 'LLMS_CLI_VERSION' ) ) { + define( 'LLMS_CLI_VERSION', $this->version ); + } + + // Get started (after REST). + add_action( 'plugins_loaded', array( $this, 'init' ) ); + + } + + /** + * Add all LifterLMS CLI commands + * + * This includes a separate file so that commands can be included on their own + * when generating documentation. + * + * @since 0.0.1 + * + * @return void + */ + public function commands() { + require_once LLMS_CLI_PLUGIN_DIR . 'src/commands.php'; + } + + /** + * Register WP_CLI hooks + * + * Loads all commands and sets up license and addon commands to be aborted + * if the LifterLMS Helper is not present. + * + * @since 0.0.1 + * + * @return void + */ + private function hooks() { + + \WP_CLI::add_hook( 'after_wp_load', array( $this, 'commands' ) ); + + // If the Helper doesn't exist abort command addition. + if ( ! class_exists( 'LifterLMS_Helper' ) ) { + $helper_commands = array( + 'license', + 'addon install', + 'addon uninstall', + 'addon activate', + 'addon deactivate', + 'addon update', + ); + foreach ( $helper_commands as $command ) { + \WP_CLI::add_hook( + "before_add_command:llms {$command}", + function( CommandAddition $command_addition ) { + $command_addition->abort( 'The LifterLMS Helper is required to use this command.' ); + } + ); + } + } + + } + /** + * Include all required files and classes + * + * @since [version + * + * @return void + */ + public function init() { + + // Only load if we have the minimum LifterLMS version installed & activated. + if ( function_exists( 'llms' ) && version_compare( '5.0.0', llms()->version, '<=' ) ) { + + $this->hooks(); + + } + + } + +} diff --git a/libraries/lifterlms-cli/src/commands.php b/libraries/lifterlms-cli/src/commands.php new file mode 100644 index 0000000000..e97b5f5d89 --- /dev/null +++ b/libraries/lifterlms-cli/src/commands.php @@ -0,0 +1,42 @@ +<?php +/** + * Load LifterLMS CLI classes + * + * @package LifterLMS/CLI + * + * @since 0.0.1 + * @version 0.0.1 + */ + +namespace LifterLMS\CLI; + +use WP_CLI; +use LifterLMS\CLI\Commands\Restful\Runner; + +/** + * Root Command + * + * @since 0.0.1 + */ +WP_CLI::add_command( 'llms', 'LifterLMS\CLI\Commands\Root' ); + +/** + * Add-on Command + * + * @since 0.0.1 + */ +WP_CLI::add_command( 'llms addon', 'LifterLMS\CLI\Commands\AddOn\Main' ); + +/** + * License Command + * + * @since 0.0.1 + */ +WP_CLI::add_command( 'llms license', 'LifterLMS\CLI\Commands\License' ); + +/** + * Restful Commands + * + * @since 0.0.1 + */ +Runner::after_wp_load(); diff --git a/libraries/lifterlms-cli/src/index.php b/libraries/lifterlms-cli/src/index.php new file mode 100644 index 0000000000..bba9ee3cf5 --- /dev/null +++ b/libraries/lifterlms-cli/src/index.php @@ -0,0 +1 @@ +<?php // Quiet. diff --git a/libraries/lifterlms-cli/vendor/autoload.php b/libraries/lifterlms-cli/vendor/autoload.php new file mode 100644 index 0000000000..74efca5643 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInitaa475372d1afb7f112bf50e9b8859e3a::getLoader(); diff --git a/libraries/lifterlms-cli/vendor/composer/ClassLoader.php b/libraries/lifterlms-cli/vendor/composer/ClassLoader.php new file mode 100644 index 0000000000..0cd6055d1b --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/ClassLoader.php @@ -0,0 +1,572 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array<string, array<string, int>> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array<string, array<int, string>> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array<string, string> + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array<string, array<string, string[]>> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array<string, string> + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array<string, string> + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array<string, bool> + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array<string, array<int, string>> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array<string, string> + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array<string, string> + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-var array<string, string> + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array<string, string> $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + * @private + */ +function includeFile($file) +{ + include $file; +} diff --git a/libraries/lifterlms-cli/vendor/composer/InstalledVersions.php b/libraries/lifterlms-cli/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000000..d50e0c9fcc --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/InstalledVersions.php @@ -0,0 +1,350 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = require __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + $installed[] = self::$installed; + + return $installed; + } +} diff --git a/libraries/lifterlms-cli/vendor/composer/LICENSE b/libraries/lifterlms-cli/vendor/composer/LICENSE new file mode 100644 index 0000000000..f27399a042 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. + diff --git a/libraries/lifterlms-cli/vendor/composer/autoload_classmap.php b/libraries/lifterlms-cli/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000000..b26f1b13b1 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/libraries/lifterlms-cli/vendor/composer/autoload_namespaces.php b/libraries/lifterlms-cli/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000000..b7fc0125db --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/libraries/lifterlms-cli/vendor/composer/autoload_psr4.php b/libraries/lifterlms-cli/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000000..b550ecf43d --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/autoload_psr4.php @@ -0,0 +1,10 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'LifterLMS\\CLI\\' => array($baseDir . '/src'), +); diff --git a/libraries/lifterlms-cli/vendor/composer/autoload_real.php b/libraries/lifterlms-cli/vendor/composer/autoload_real.php new file mode 100644 index 0000000000..78fa4409f3 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/autoload_real.php @@ -0,0 +1,57 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInitaa475372d1afb7f112bf50e9b8859e3a +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + /** + * @return \Composer\Autoload\ClassLoader + */ + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + require __DIR__ . '/platform_check.php'; + + spl_autoload_register(array('ComposerAutoloaderInitaa475372d1afb7f112bf50e9b8859e3a', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); + spl_autoload_unregister(array('ComposerAutoloaderInitaa475372d1afb7f112bf50e9b8859e3a', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitaa475372d1afb7f112bf50e9b8859e3a::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/libraries/lifterlms-cli/vendor/composer/autoload_static.php b/libraries/lifterlms-cli/vendor/composer/autoload_static.php new file mode 100644 index 0000000000..2a3b404912 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInitaa475372d1afb7f112bf50e9b8859e3a +{ + public static $prefixLengthsPsr4 = array ( + 'L' => + array ( + 'LifterLMS\\CLI\\' => 14, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'LifterLMS\\CLI\\' => + array ( + 0 => __DIR__ . '/../..' . '/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitaa475372d1afb7f112bf50e9b8859e3a::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitaa475372d1afb7f112bf50e9b8859e3a::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitaa475372d1afb7f112bf50e9b8859e3a::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/libraries/lifterlms-cli/vendor/composer/installed.json b/libraries/lifterlms-cli/vendor/composer/installed.json new file mode 100644 index 0000000000..f20a6c47c6 --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": false, + "dev-package-names": [] +} diff --git a/libraries/lifterlms-cli/vendor/composer/installed.php b/libraries/lifterlms-cli/vendor/composer/installed.php new file mode 100644 index 0000000000..252bd7959e --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/installed.php @@ -0,0 +1,23 @@ +<?php return array( + 'root' => array( + 'pretty_version' => 'dev-trunk', + 'version' => 'dev-trunk', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '82418fb524fe978ee668d33ff54bfd5d6e125cb8', + 'name' => 'lifterlms/lifterlms-cli', + 'dev' => false, + ), + 'versions' => array( + 'lifterlms/lifterlms-cli' => array( + 'pretty_version' => 'dev-trunk', + 'version' => 'dev-trunk', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '82418fb524fe978ee668d33ff54bfd5d6e125cb8', + 'dev_requirement' => false, + ), + ), +); diff --git a/libraries/lifterlms-cli/vendor/composer/platform_check.php b/libraries/lifterlms-cli/vendor/composer/platform_check.php new file mode 100644 index 0000000000..92370c5a0c --- /dev/null +++ b/libraries/lifterlms-cli/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ +<?php + +// platform_check.php @generated by Composer + +$issues = array(); + +if (!(PHP_VERSION_ID >= 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/libraries/lifterlms-helper/CHANGELOG.md b/libraries/lifterlms-helper/CHANGELOG.md new file mode 100644 index 0000000000..84692099b0 --- /dev/null +++ b/libraries/lifterlms-helper/CHANGELOG.md @@ -0,0 +1,214 @@ +LifterLMS Helper Changelog +========================== + +v3.4.1 - 2021-08-17 +------------------- + ++ Fixed undefined index error encountered when programmatically deactivating a key that was not previously activated on the site. + + +v3.4.0 - 2021-08-04 +------------------- + +##### Localization updates + ++ Only runs localization functions when loaded as an independent plugin. ++ Replace the textdoman 'lifterlms-helper' with 'lifterlms'. + +##### Updates + ++ Use `llms_helper()` in favor of deprecated `LLMS_Helper()` in various locations. + +##### Bugfix + ++ Don't attempt to run migrations from versions less than 3.0.0 during first run when loaded as a library. + + +v3.3.1 - 2021-07-26 +------------------- + ++ Load `llms_helper()->upgrader` WP_CLI context in preparation for forthcoming the `lifterlms-cli`. + + +v3.3.0 - 2021-06-14 +------------------- + ++ This plugin is now included by default via the LifterLMS core in versions 5.0+. Installing this plugin directly will use the plugin version instead of the version included with the core. Direct installation is likely only required for development purposes when using LifterLMS 5.0+. ++ The main function `llms_helper()` is declared conditionally when the class `LifterLMS_Helper` is not yet declared. ++ Added a constant `LLMS_HELPER_DISABLE` which allows disabling of the plugin. ++ Distribution release zips now include a `composer.json` file to allow for installation via composer. + + +v3.2.1 - 2021-06-03 +------------------- + +##### Updates + ++ Flush cached update and add-on data when adding or removing license keys and when changing channel subscription for a package. ++ Enable updating to beta versions of packages that don't require a license when no license is present. + + +v3.2.0 - 2020-12-02 +------------------- + +##### Updates + ++ Moved the class `LifterLMS_Helper` class to its own file from `lifterlms-helper.php`. ++ Use `self::$instance` in favor of `self::$_instance`. ++ Use `llms()` in favor of deprecated `LLMS()`. ++ Use `llms_filter_input()` to access `$_POST` data in various places. ++ Use strict comparison for `in_array()`. + +##### Bug fixes + ++ Fixed usage of incorrect textdomain in various places. + +##### Deprecations + ++ Replaced usage of protected class property `$instance` in favor of `$_instance` in various singleton classes. ++ Function `LLMS_Helper()` is deprecated in favor of `llms_helper()`. ++ File `includes/model-llms-helper-add-on.php` is deprecated, use `includes/models/class-llms-helper-add-on.php` instead. + + +v3.1.0 - 2020-05-22 +------------------- + ++ Load changelogs from the make.lifterlms.com release notes archive in favor of from static html files. ++ Remove reliance on `file_get_contents()` causing errors on servers without access to the function. + + +v3.0.2 - 2018-08-29 +------------------- + ++ Fixed fatal errors encountered as a result of failed API calls ++ Fixed broken links output on the plugins update screen when an add-on is unlicensed and has an update available ++ Fixed issue causing non-beta versions of the LifterLMS core to be served from LifterLMS.com instead of from WordPress.org + + +v3.0.1 - 2018-08-02 +------------------- + ++ Fixed an issue causing key migration to run on the frontend resulting in a fatal error related to missing admin-only functions ++ Fixed an issue causing multiple submitted keys to not work properly on certain environments ++ Fixed issue causing installation script to make an activation API call even when no keys exist ++ Improved installation script message to only display a migration message when keys are actually migrated + + +v3.0.0 - 2018-08-01 +------------------- + ++ **This is nearly a complete rewrite of the codebase. Things have moved but no features have been removed.** ++ Requires LifterLMS version 3.22.0 or later ++ License key activation is now on a per-site basis as opposed to a per product basis. This means that if you have a license key for a bundle you don't have to enter the key for each add-on, you enter the key only once and it will activate ALL the add-ons. ++ The "Licenses" tab has been removed and your add-ons and licenses are now managed via LifterLMS -> Add-ons & More ++ A migration script exists to move license keys from previous versions of the helper to this version. After upgrading check LifterLMS -> Add-ons & More to ensure your keys were successfully migrated. ++ You can now install add-ons through the this plugin without having to download and install them manually. Enter your license key(s) and select the add-ons you wish to install to have them installed automatically. You can bulk install as well. ++ You can now subscribe to beta channels of LifterLMS and any LifterLMS add-ons. Visit the LifterLMS -> Status -> Betas screen to subscribe to betas. Always use betas at your own risk, by nature they're unstable! ++ Uses the LifterLMS.com v3 REST api for all API calls ++ Added RTL language support ++ Added i18n support ++ Removed and replaced various functions ++ Fixes many bugs and almost certainly introduces some new ones + + +v2.5.1 - 2017-11-08 +------------------- + ++ Fix issue causing false activations which cannot be deactivated due to blank activation keys + + +v2.5.0 - 2017-07-18 +------------------- + ++ Allow add-ons to be bulk deactivated ++ Integrates with LifterLMS site clone detection in order to automatically activate plugins on your new URL when cloning to staging / production. ++ Following clone detection if activation fails the plugin will no longer show the add-ons as activated (since they're not activated on the new URL) ++ Minor admin-panel performance improvements ++ Now uses minified JS and CSS assets ++ Now fully translateable! + + +v2.4.3 - 2017-02-09 +------------------- + ++ Handle undefined errors during post plugin install from zip file + + +v2.4.2 - 2017-01-20 +------------------- + ++ Handle failed api calls gracefully + + +v2.4.1 - 2016-12-30 +------------------- + ++ Cache add-on list prior to filtering + + +v2.4.0 - 2016-12-20 +------------------- + ++ Added a unified Helper sceen accessible via LifterLMS -> Settings -> Helper ++ Activate multiple addons simultaneously via one API call ++ Site deactivation now deactivates from remote activation server in addition to local deactivation ++ Upgraded database key handling prevents accidental duplicate activation attempts ++ Fixed several undefined index warnings ++ Normalized option fields keys + + +v2.3.1 - 2016-10-12 +------------------- + ++ Fixes issue with theme upgrade post install not working resulting in themes existing in the wrong directory after an upgrade + + +v2.3.0 - 2016-10-10 +------------------- + ++ Significantly upgrades the speed of version checks. Previously checked each LifterLMS Add-on separately, now makes one API call to retreive versions of all installed LifterLMS Add-ons. ++ Adds support for the Universe Bundle which is one key associated with multiple products + + +v2.2.0 - 2016-07-06 +------------------- + ++ After updates, clear cached update data so the upgrade doesn't still appear as pending ++ After changing license keys, clear cahced data so the next upgrade attempt will not fail again (unless it's still supposed to fail) ++ After updating the currently active theme, correctly reactivate the theme + + +v2.1.0 - 2016-06-14 +------------------- + ++ Prevent hijacking the LifterLMS Core lightbox data when attempting to view update details on the plugin update screen. ++ Added [Parsedown](https://github.com/erusev/parsedown) to render Markdown style changelogs into HTML when viewing extension changelogs in the the lightbox on plugin update screens. + + +v2.0.0 - 2016-04-08 +------------------- + ++ Includes theme-related APIs for serving updates for themes ++ Better error reporting and handling ++ A few very exciting performance enhancements + + +v1.0.2 - 2016-03-07 +------------------- + ++ Fixed an undefined variable which produced a php warning when `WP_DEBUG` was enabled ++ Resolved an issue that caused the LifterLMS Helper to hijack the "details" and related plugin screens that display inside a lightbox in the plugins admin page. ++ Added a .editorconfig file ++ Added changelog file + + +v1.0.1 - 2016-02-11 +------------------- + ++ Actual public release + + +v1.0.0 - 2016-02-10 +------------------- + ++ Initial public release diff --git a/libraries/lifterlms-helper/assets/css/llms-helper-rtl.css b/libraries/lifterlms-helper/assets/css/llms-helper-rtl.css new file mode 100644 index 0000000000..939e4bba1d --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper-rtl.css @@ -0,0 +1,77 @@ +.wrap.lifterlms-addons .llms-licenses { + display: inline-block; + margin-right: 20px; + position: relative; + vertical-align: middle; + z-index: 1; +} +.wrap.lifterlms-addons .llms-licenses .llms-license-header { + margin: 0; +} +.wrap.lifterlms-addons .llms-licenses label { + display: block; + margin-top: 20px; +} +.wrap.lifterlms-addons .llms-licenses label:first-child { + margin-top: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys { + list-style-type: none; + margin: 5px 0; + padding: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys li { + margin: 0 0 5px; + padding: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys li input:checked + span { + color: #e5554e; + font-style: italic; +} +.wrap.lifterlms-addons .llms-licenses .fa-chevron-down { + margin-right: 10px; +} +.wrap.lifterlms-addons .llms-licenses .llms-key-field { + background: #fff; + border: 1px solid #ddd; + display: none; + margin-right: 0; + position: absolute; + right: -1px; + padding: 20px; + top: calc( 100% - 2px ); + width: 340px; +} +.wrap.lifterlms-addons .llms-licenses .llms-key-field textarea { + display: inline-block; + height: 86px; + font-size: 14px; + font-family: monospace; + line-height: 1.8; + margin: 5px 0; + padding: 5px 10px; + resize: none; + vertical-align: middle; + width: 100%; +} + +@media only screen and (min-width: 800px) { + .llms-status--betas .llms-beta-main { + display: flex; + } + .llms-status--betas .llms-beta-table { + flex: 2; + } + .llms-status--betas .llms-beta-aside { + flex: 1; + margin-left: 20px; + } +} +.llms-status--betas .llms-beta-aside { + background: #fef7f7; + border: 1px solid #e5554e; + padding: 20px; +} +.llms-status--betas .llms-beta-aside h1 { + padding-top: 0; +} diff --git a/libraries/lifterlms-helper/assets/css/llms-helper-rtl.min.css b/libraries/lifterlms-helper/assets/css/llms-helper-rtl.min.css new file mode 100644 index 0000000000..be40f5e9ce --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper-rtl.min.css @@ -0,0 +1 @@ +.wrap.lifterlms-addons .llms-licenses{display:inline-block;margin-right:20px;position:relative;vertical-align:middle;z-index:1}.wrap.lifterlms-addons .llms-licenses .llms-license-header{margin:0}.wrap.lifterlms-addons .llms-licenses label{display:block;margin-top:20px}.wrap.lifterlms-addons .llms-licenses label:first-child{margin-top:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys{list-style-type:none;margin:5px 0;padding:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys li{margin:0 0 5px;padding:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys li input:checked+span{color:#e5554e;font-style:italic}.wrap.lifterlms-addons .llms-licenses .fa-chevron-down{margin-right:10px}.wrap.lifterlms-addons .llms-licenses .llms-key-field{background:#fff;border:1px solid #ddd;display:none;margin-right:0;position:absolute;right:-1px;padding:20px;top:calc( 100% - 2px );width:340px}.wrap.lifterlms-addons .llms-licenses .llms-key-field textarea{display:inline-block;height:86px;font-size:14px;font-family:monospace;line-height:1.8;margin:5px 0;padding:5px 10px;resize:none;vertical-align:middle;width:100%}@media only screen and (min-width: 800px){.llms-status--betas .llms-beta-main{display:flex}.llms-status--betas .llms-beta-table{flex:2}.llms-status--betas .llms-beta-aside{flex:1;margin-left:20px}}.llms-status--betas .llms-beta-aside{background:#fef7f7;border:1px solid #e5554e;padding:20px}.llms-status--betas .llms-beta-aside h1{padding-top:0} diff --git a/libraries/lifterlms-helper/assets/css/llms-helper.css b/libraries/lifterlms-helper/assets/css/llms-helper.css new file mode 100644 index 0000000000..adba48fc1c --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper.css @@ -0,0 +1,79 @@ +.wrap.lifterlms-addons .llms-licenses { + display: inline-block; + margin-left: 20px; + position: relative; + vertical-align: middle; + z-index: 1; +} +.wrap.lifterlms-addons .llms-licenses .llms-license-header { + margin: 0; +} +.wrap.lifterlms-addons .llms-licenses label { + display: block; + margin-top: 20px; +} +.wrap.lifterlms-addons .llms-licenses label:first-child { + margin-top: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys { + list-style-type: none; + margin: 5px 0; + padding: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys li { + margin: 0 0 5px; + padding: 0; +} +.wrap.lifterlms-addons .llms-licenses .llms-active-keys li input:checked + span { + color: #e5554e; + font-style: italic; +} +.wrap.lifterlms-addons .llms-licenses .fa-chevron-down { + margin-left: 10px; +} +.wrap.lifterlms-addons .llms-licenses .llms-key-field { + background: #fff; + border: 1px solid #ddd; + display: none; + margin-left: 0; + position: absolute; + left: -1px; + padding: 20px; + top: calc( 100% - 2px ); + width: 340px; +} +.wrap.lifterlms-addons .llms-licenses .llms-key-field textarea { + display: inline-block; + height: 86px; + font-size: 14px; + font-family: monospace; + line-height: 1.8; + margin: 5px 0; + padding: 5px 10px; + resize: none; + vertical-align: middle; + width: 100%; +} + +@media only screen and (min-width: 800px) { + .llms-status--betas .llms-beta-main { + display: flex; + } + .llms-status--betas .llms-beta-table { + flex: 2; + } + .llms-status--betas .llms-beta-aside { + flex: 1; + margin-right: 20px; + } +} +.llms-status--betas .llms-beta-aside { + background: #fef7f7; + border: 1px solid #e5554e; + padding: 20px; +} +.llms-status--betas .llms-beta-aside h1 { + padding-top: 0; +} + +/*# sourceMappingURL=llms-helper.css.map */ diff --git a/libraries/lifterlms-helper/assets/css/llms-helper.css.map b/libraries/lifterlms-helper/assets/css/llms-helper.css.map new file mode 100644 index 0000000000..9009cad586 --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../scss/llms-helper.scss"],"names":[],"mappings":"AAKC;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;AACA;EAAgB;;AAGjB;EACC;EACA;EACA;;AACA;EACC;EACA;;AACA;EACC,OA9BO;EA+BP;;AAKH;EAAmB;;AAEnB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAWH;EAEC;IACC;;EAED;IACC;;EAED;IACC;IACA;;;AAKF;EACC;EACA;EACA;;AAEA;EACC","file":"llms-helper.css"} \ No newline at end of file diff --git a/libraries/lifterlms-helper/assets/css/llms-helper.min.css b/libraries/lifterlms-helper/assets/css/llms-helper.min.css new file mode 100644 index 0000000000..385cae954e --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper.min.css @@ -0,0 +1 @@ +.wrap.lifterlms-addons .llms-licenses{display:inline-block;margin-left:20px;position:relative;vertical-align:middle;z-index:1}.wrap.lifterlms-addons .llms-licenses .llms-license-header{margin:0}.wrap.lifterlms-addons .llms-licenses label{display:block;margin-top:20px}.wrap.lifterlms-addons .llms-licenses label:first-child{margin-top:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys{list-style-type:none;margin:5px 0;padding:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys li{margin:0 0 5px;padding:0}.wrap.lifterlms-addons .llms-licenses .llms-active-keys li input:checked+span{color:#e5554e;font-style:italic}.wrap.lifterlms-addons .llms-licenses .fa-chevron-down{margin-left:10px}.wrap.lifterlms-addons .llms-licenses .llms-key-field{background:#fff;border:1px solid #ddd;display:none;margin-left:0;position:absolute;left:-1px;padding:20px;top:calc( 100% - 2px );width:340px}.wrap.lifterlms-addons .llms-licenses .llms-key-field textarea{display:inline-block;height:86px;font-size:14px;font-family:monospace;line-height:1.8;margin:5px 0;padding:5px 10px;resize:none;vertical-align:middle;width:100%}@media only screen and (min-width: 800px){.llms-status--betas .llms-beta-main{display:flex}.llms-status--betas .llms-beta-table{flex:2}.llms-status--betas .llms-beta-aside{flex:1;margin-right:20px}}.llms-status--betas .llms-beta-aside{background:#fef7f7;border:1px solid #e5554e;padding:20px}.llms-status--betas .llms-beta-aside h1{padding-top:0}/*# sourceMappingURL=llms-helper.min.css.map */ diff --git a/libraries/lifterlms-helper/assets/css/llms-helper.min.css.map b/libraries/lifterlms-helper/assets/css/llms-helper.min.css.map new file mode 100644 index 0000000000..cd6ffcdd0f --- /dev/null +++ b/libraries/lifterlms-helper/assets/css/llms-helper.min.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../scss/llms-helper.scss"],"names":[],"mappings":"AAKC,sCACC,qBACA,iBACA,kBACA,sBACA,UAEA,2DACC,SAGD,4CACC,cACA,gBACA,qEAGD,wDACC,qBACA,aACA,UACA,2DACC,eACA,UACA,8EACC,MA9BO,QA+BP,kBAKH,wEAEA,sDACC,gBACA,sBACA,aACA,cACA,kBACA,UACA,aACA,uBACA,YAEA,+DACC,qBACA,YACA,eACA,sBACA,gBACA,aACA,iBACA,YACA,sBACA,WAWH,0CAEC,oCACC,aAED,qCACC,OAED,qCACC,OACA,mBAKF,qCACC,mBACA,yBACA,aAEA,wCACC","file":"llms-helper.min.css"} \ No newline at end of file diff --git a/libraries/lifterlms-helper/class-lifterlms-helper.php b/libraries/lifterlms-helper/class-lifterlms-helper.php new file mode 100644 index 0000000000..cf706b2b02 --- /dev/null +++ b/libraries/lifterlms-helper/class-lifterlms-helper.php @@ -0,0 +1,216 @@ +<?php +/** + * LifterLMS Helper main class + * + * @package LifterLMS_Helper/Main + * + * @since 3.2.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LifterLMS_Helper class + * + * @since 1.0.0 + * @since 3.2.0 Moved class to its own file from `lifterlms-helper.php`. + * Replaced class variable `$_instance` with `$instance`. + */ +final class LifterLMS_Helper { + + /** + * Current Plugin Version + * + * @var string + */ + public $version = '3.4.1'; + + /** + * Singleton instance reference + * + * @var null + */ + protected static $instance = null; + + /** + * Instance of the LLMS_Helper_Upgrader class + * + * Use/retrieve via llms_helper()->upgrader(). + * + * @var null|LLMS_Helper_Upgrader + */ + private $upgrader = null; + + /** + * Retrieve the main Instance of LifterLMS_Helper + * + * @since 3.0.0 + * @since 3.2.0 Use `self::$instance` in favor of `self::$_instance`. + * + * @return LifterLMS_Helper + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Constructor, get things started! + * + * @since 1.0.0 + * @since 3.4.0 Only localize when loaded as an independent plugin. + * + * @return void + */ + private function __construct() { + + // Define class constants. + $this->define_constants(); + + /** + * When loaded as a library included by the LifterLMS core localization is handled by the LifterLMS core. + * + * When the plugin is loaded by itself as a plugin, we must localize it independently. + */ + if ( ! defined( 'LLMS_HELPER_LIB' ) || ! LLMS_HELPER_LIB ) { + add_action( 'init', array( $this, 'load_textdomain' ), 0 ); + } + + add_action( 'plugins_loaded', array( $this, 'init' ) ); + + } + + /** + * Inititalize the Plugin + * + * @since 1.0.0 + * @since 3.0.0 Unknown. + * @since 3.2.0 Use `llms()` in favor of deprecated `LLMS()`. + * @since 3.3.1 Load the upgrader instance in WP_CLI context. + * + * @return void + */ + public function init() { + + // Only load if we have the minimum LifterLMS version installed & activated. + if ( function_exists( 'llms' ) && version_compare( '3.22.0', llms()->version, '<=' ) ) { + + $this->includes(); + $this->crons(); + + if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { + $this->upgrader = LLMS_Helper_Upgrader::instance(); + } + } + + } + + /** + * Schedule and handle cron functions + * + * @since 3.0.0 + * + * @return void + */ + private function crons() { + + add_action( 'llms_helper_check_license_keys', array( 'LLMS_Helper_Keys', 'check_keys' ) ); + + if ( ! wp_next_scheduled( 'llms_helper_check_license_keys' ) ) { + wp_schedule_event( time(), 'daily', 'llms_helper_check_license_keys' ); + } + + } + + /** + * Define constants for plugin + * + * @since 1.0.0 + * + * @return void + */ + private function define_constants() { + + if ( ! defined( 'LLMS_HELPER_VERSION' ) ) { + define( 'LLMS_HELPER_VERSION', $this->version ); + } + + } + + /** + * Include all clasess required by the plugin + * + * @since 1.0.0 + * @since 3.0.0 Include new files. + * + * @return void + */ + private function includes() { + + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-admin-add-ons.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-assets.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-betas.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-cloned.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-install.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-keys.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-options.php'; + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/class-llms-helper-upgrader.php'; + + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/models/class-llms-helper-add-on.php'; + + require_once LLMS_HELPER_PLUGIN_DIR . 'includes/functions-llms-helper.php'; + + } + + /** + * Load l10n files. + * + * This method is only used when the plugin is loaded as a standalone plugin (for development purposes), + * otherwise (when loaded as a library from within the LifterLMS core plugin) the localization + * strings are included into the LifterLMS Core plugin's po/mo files and are localized by the LifterLMS + * core plugin. + * + * Files can be found in the following order (The first loaded file takes priority): + * 1. WP_LANG_DIR/lifterlms/lifterlms-rest-LOCALE.mo + * 2. WP_LANG_DIR/plugins/lifterlms-rest-LOCALE.mo + * 3. WP_CONTENT_DIR/plugins/lifterlms-rest/i18n/lifterlms-rest-LOCALE.mo + * + * Note: The function `load_plugin_textdomain()` is not used because the same textdomain as the LifterLMS core + * is used for this plugin but the file is named `lifterlms-rest` in order to allow using a separate language + * file for each codebase. + * + * @since 2.5.0 + * @since 3.4.0 Updated to the core textdomain. + * + * @return void + */ + public function load_textdomain() { + + // Load locale. + $locale = apply_filters( 'plugin_locale', get_locale(), 'lifterlms' ); + + // Load from the LifterLMS "safe" directory if it exists. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/lifterlms/lifterlms-helper-' . $locale . '.mo' ); + + // Load from the default plugins language file directory. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/plugins/lifterlms-helper-' . $locale . '.mo' ); + + // Load from the plugin's language file directory. + load_textdomain( 'lifterlms', LLMS_HELPER_PLUGIN_DIR . '/i18n/lifterlms-helper-' . $locale . '.mo' ); + } + + /** + * Return the singleton instance of the LLMS_Helper_Upgader + * + * @since 3.0.0 + * + * @return LLMS_Helper_Upgrader + */ + public function upgrader() { + return $this->upgrader; + } + +} diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-admin-add-ons.php b/libraries/lifterlms-helper/includes/class-llms-helper-admin-add-ons.php new file mode 100644 index 0000000000..49f3a54642 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-admin-add-ons.php @@ -0,0 +1,405 @@ +<?php +/** + * Modify the admin add-ons page + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Admin_Add_Ons + * + * @since 3.0.0 + */ +class LLMS_Helper_Admin_Add_Ons { + + /** + * Caches current state of the sites keys + * + * Use $this->has_keys() to retrieve the value. + * + * @var bool + */ + private $has_keys = null; + + /** + * Constructor + * + * @since 3.0.0 + */ + public function __construct() { + + add_action( 'admin_init', array( $this, 'handle_actions' ) ); + + // Output navigation items. + add_action( 'lifterlms_before_addons_nav', array( $this, 'output_navigation_items' ) ); + + // Output the license manager interface button / dropdown. + add_action( 'llms_addons_page_after_title', array( $this, 'output_license_manager' ) ); + + // Filter current section default. + add_filter( 'llms_admin_add_ons_get_current_section', array( $this, 'filter_get_current_section' ) ); + + // Filter the content display for a section. + add_filter( 'llms_admin_add_ons_get_current_section_default_content', array( $this, 'filter_get_current_section_content' ), 10, 2 ); + + // Add install & update actions to the list of available management actions powered by the bulk actions functions in core. + add_filter( 'llms_admin_add_ons_manage_actions', array( $this, 'filter_manage_actions' ) ); + + // Output html for helper-powered actions (install & update). + add_action( 'llms_add_ons_single_item_actions', array( $this, 'output_single_install_action' ), 5, 2 ); + add_action( 'llms_add_ons_single_item_after_actions', array( $this, 'output_single_update_action' ), 5, 2 ); + + add_filter( 'llms_admin_addon_features_exclude_ids', array( $this, 'filter_feature_exclude_ids' ) ); + + } + + /** + * Change the default section from "All" to "Mine" but only if license keys have been saved + * + * @since 3.0.0 + * + * @param string $section Section slug. + * @return string + */ + public function filter_get_current_section( $section ) { + + if ( 'all' === $section && empty( $_GET['section'] ) && $this->has_keys() ) { + return 'mine'; + } + + return $section; + + } + + /** + * Add "mine" tab content + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * + * @param array $content Default items to display. + * @param string $section Current tab slug. + * @return array + */ + public function filter_get_current_section_content( $content, $section ) { + + if ( 'mine' === $section ) { + $mine = llms_helper_get_available_add_ons(); + $addons = llms_get_add_ons(); + if ( ! is_wp_error( $addons ) && isset( $addons['items'] ) ) { + foreach ( $addons['items'] as $item ) { + if ( in_array( $item['id'], $mine ) ) { + $content[] = $item; + } + } + } + } + + return $content; + + } + + /** + * Exclude IDs for all add-ons that are currently available on the site + * + * @since 3.0.0 + * + * @param array $ids Existing product ids to exclude. + * @return array + */ + public function filter_feature_exclude_ids( $ids ) { + return array_unique( array_merge( $ids, llms_helper_get_available_add_ons( false ) ) ); + } + + /** + * Add installatino & update actions to the list of available management actions + * + * @since 3.0.0 + * + * @param array $actions List of available actions, the action should correspond to a method in the LLMS_Helper_Add_On class. + * @return array + */ + public function filter_manage_actions( $actions ) { + return array_merge( array( 'install', 'update' ), $actions ); + } + + /** + * Handle form submission actions + * + * @since 3.0.0 + * @since 3.2.0 Let the LifterLMS Core output flashed notices + * @since 3.2.1 Flush cached addon and package update data when adding or removing keys. + * + * @return void + */ + public function handle_actions() { + + // License key addition & removal. + if ( ! llms_verify_nonce( '_llms_manage_keys_nonce', 'llms_manage_keys' ) ) { + return; + } + + $flush = false; + + if ( isset( $_POST['llms_activate_keys'] ) && ! empty( $_POST['llms_add_keys'] ) ) { + + $flush = true; + $this->handle_activations(); + + } elseif ( isset( $_POST['llms_deactivate_keys'] ) && ! empty( $_POST['llms_remove_keys'] ) ) { + + $flush = true; + $this->handle_deactivations(); + + } + + if ( $flush ) { + llms_helper_flush_cache(); + } + + } + + /** + * Activate license keys with LifterLMS.com api + * + * Output errors / successes & saves successful keys to the db. + * + * @since 3.0.0 + * @since 3.2.0 Don't access $_POST directly. + * @since 3.4.0 Use core textdomain. + * + * @return void + */ + private function handle_activations() { + + $res = LLMS_Helper_Keys::activate_keys( llms_filter_input( INPUT_POST, 'llms_add_keys', FILTER_SANITIZE_STRING ) ); + + if ( is_wp_error( $res ) ) { + LLMS_Admin_Notices::flash_notice( $res->get_error_message(), 'error' ); + return; + } + + $data = $res['data']; + if ( isset( $data['errors'] ) ) { + foreach ( $data['errors'] as $error ) { + LLMS_Admin_Notices::flash_notice( make_clickable( $error ), 'error' ); + } + } + + if ( isset( $data['activations'] ) ) { + foreach ( $data['activations'] as $activation ) { + LLMS_Helper_Keys::add_license_key( $activation ); + // Translators: %s = License key. + LLMS_Admin_Notices::flash_notice( sprintf( __( '"%s" has been saved!', 'lifterlms' ), $activation['license_key'] ), 'success' ); + } + } + + } + + /** + * Deactivate license keys with LifterLMS.com api + * + * Output errors / successes & removes keys from the db. + * + * @since 3.0.0 + * @since 3.2.0 Don't access $_POST directly. + * @since 3.4.0 Use core textdomain. + * + * @return void + */ + private function handle_deactivations() { + + $keys = llms_filter_input( INPUT_POST, 'llms_remove_keys', FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY ); + $res = LLMS_Helper_Keys::deactivate_keys( $keys ); + + if ( is_wp_error( $res ) ) { + LLMS_Admin_Notices::flash_notice( $res->get_error_message(), 'error' ); + return; + } + + foreach ( $keys as $key ) { + LLMS_Helper_Keys::remove_license_key( $key ); + /* Translators: %s = License Key */ + LLMS_Admin_Notices::flash_notice( sprintf( __( 'License key "%s" was removed from this site.', 'lifterlms' ), $key ), 'info' ); + } + + if ( isset( $data['errors'] ) ) { + foreach ( $data['errors'] as $error ) { + LLMS_Admin_Notices::flash_notice( make_clickable( $error ), 'error' ); + } + } + + } + + /** + * Determine if the current site has active license keys + * + * @since 3.0.0 + * + * @return bool + */ + public function has_keys() { + + if ( is_null( $this->has_keys ) ) { + $this->has_keys = ( count( llms_helper_options()->get_license_keys() ) ); + } + + return $this->has_keys; + + } + + /** + * Output the HTML for the license manager area + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @return void + */ + public function output_license_manager() { + + $my_keys = llms_helper_options()->get_license_keys(); + if ( $my_keys ) { + wp_enqueue_style( 'plugin-install' ); + wp_enqueue_script( 'plugin-install' ); + add_thickbox(); + } + + ?> + <section class="llms-licenses"> + <button class="llms-button-primary" id="llms-active-keys-toggle"> + <?php _e( 'My License Keys', 'lifterlms' ); ?> + <i class="fa fa-chevron-down" aria-hidden="true"></i> + </button> + + <form action="" class="llms-key-field" id="llms-key-field-form" method="POST"> + + <?php if ( $my_keys ) : ?> + <h4 class="llms-license-header"><?php _e( 'Manage Saved License Keys', 'lifterlms' ); ?></h4> + <ul class="llms-active-keys"> + <?php foreach ( $my_keys as $key ) : ?> + <li> + <label for="llms_key_<?php echo esc_attr( $key['license_key'] ); ?>"> + <input id="llms_key_<?php echo esc_attr( $key['license_key'] ); ?>" name="llms_remove_keys[]" type="checkbox" value="<?php echo esc_attr( $key['license_key'] ); ?>"> + <span><?php echo $key['license_key']; ?></span> + </label> + </li> + + <?php endforeach; ?> + </ul> + <button class="llms-button-danger small" name="llms_deactivate_keys" type="submit"><?php _e( 'Remove Selected', 'lifterlms' ); ?></button> + <?php endif; ?> + + <label for="llms_keys_field"> + <h4 class="llms-license-header"><?php _e( 'Add New License Keys', 'lifterlms' ); ?></h4> + <textarea name="llms_add_keys" id="llms_keys_field" placeholder="<?php esc_attr_e( 'Enter each license on a new line', 'lifterlms' ); ?>"></textarea> + </label> + <button class="llms-button-primary small" name="llms_activate_keys" type="submit"><?php _e( 'Add New', 'lifterlms' ); ?></button> + <?php wp_nonce_field( 'llms_manage_keys', '_llms_manage_keys_nonce' ); ?> + </form> + </section> + + <?php + } + + /** + * Output html for installation action + * + * Does not output for "featured" items on general settings. + * + * @since 3.0.0 + * @since 3.2.1 Output single install action if the addon doesn't require license (e.g. free product). + * @since 3.4.0 Use core textdomain. + * + * @param obj $addon LLMS_Add_On instance. + * @param string $curr_tab Slug of the current tab being viewed. + * @return void + */ + public function output_single_install_action( $addon, $curr_tab ) { + + if ( 'featured' === $curr_tab ) { + return; + } + + if ( $addon->is_installable() && ! $addon->is_installed() && ( ! $addon->requires_license() || $addon->is_licensed() ) ) { + ?> + <label class="llms-status-icon status--<?php echo esc_attr( $addon->get_install_status() ); ?>" for="<?php echo esc_attr( sprintf( '%s-install', $addon->get( 'id' ) ) ); ?>"> + <input class="llms-bulk-check" data-action="install" name="llms_install[]" id="<?php echo esc_attr( sprintf( '%s-install', $addon->get( 'id' ) ) ); ?>" type="checkbox" value="<?php echo esc_attr( $addon->get( 'id' ) ); ?>"> + <i class="fa fa-check-square-o" aria-hidden="true"></i> + <i class="fa fa-cloud-download" aria-hidden="true"></i> + <span class="llms-status-text"><?php _e( 'Install', 'lifterlms' ); ?></span> + </label> + <a href="<?php echo admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $addon->get( 'id' ) . '§ion=changelog&TB_iframe=true&width=600&height=800' ); ?>" class="thickbox open-plugin-details-modal tip--bottom-left" data-tip="<?php esc_attr_e( 'View add-on details', 'lifterlms' ); ?>"> + <i class="fa fa-info-circle" aria-hidden="true"></i> + </span> + <?php + } + + } + + /** + * Output html for update action + * + * Does not output for "featured" items on general settings. + * + * @since 3.0.0 + * @since 3.2.1 Output single update action if the addon doesn't require license (e.g. free product). + * @since 3.4.0 Use core textdomain. + * + * @param obj $addon LLMS_Add_On instance. + * @param string $curr_tab Slug of the current tab being viewed. + * @return void + */ + public function output_single_update_action( $addon, $curr_tab ) { + + if ( 'featured' === $curr_tab ) { + return; + } + + if ( $addon->is_installable() && $addon->is_installed() && ( ! $addon->requires_license() || $addon->is_licensed() ) && $addon->has_available_update() ) { + ?> + <label class="llms-status-icon status--update-available" for="<?php echo esc_attr( sprintf( '%s-update', $addon->get( 'id' ) ) ); ?>"> + <input class="llms-bulk-check" data-action="update" name="llms_update[]" id="<?php echo esc_attr( sprintf( '%s-update', $addon->get( 'id' ) ) ); ?>" type="checkbox" value="<?php echo esc_attr( $addon->get( 'id' ) ); ?>"> + <i class="fa fa-check-square-o" aria-hidden="true"></i> + <i class="fa fa-arrow-circle-up" aria-hidden="true"></i> + <span class="llms-status-text"><?php _e( 'Update', 'lifterlms' ); ?> + </label> + <a href="<?php echo admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $addon->get( 'id' ) . '§ion=changelog&TB_iframe=true&width=600&height=800' ); ?>" class="thickbox open-plugin-details-modal tip--bottom-left" data-tip="<?php esc_attr_e( 'View update details', 'lifterlms' ); ?>"> + <i class="fa fa-info-circle" aria-hidden="true"></i> + </span> + <?php + } + + } + + /** + * Output additional navigation items + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @param string $current_section Current section slug. + * @return void + */ + public function output_navigation_items( $current_section ) { + + if ( ! $this->has_keys() ) { + return; + } + + ?> + <li class="llms-nav-item<?php echo ( 'mine' === $current_section ) ? ' llms-active' : ''; ?>"> + <a class="llms-nav-link" href="<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons§ion=mine' ) ); ?>"><?php _e( 'My Add-Ons', 'lifterlms' ); ?></a> + </li> + <?php + + } + +} + +return new LLMS_Helper_Admin_Add_Ons(); diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-assets.php b/libraries/lifterlms-helper/includes/class-llms-helper-assets.php new file mode 100644 index 0000000000..f6048744fe --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-assets.php @@ -0,0 +1,63 @@ +<?php +/** + * Enqueue Scripts & Styles + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Assets + * + * @since 3.0.0 + */ +class LLMS_Helper_Assets { + + /** + * Constructor + * + * @since 3.0.0 + * + * @return void + */ + public function __construct() { + + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); + + } + + /** + * Register, enqueue, & localize + * + * @since 3.0.0 + * + * @return void + */ + public function enqueue() { + + $load = false; + $screen = get_current_screen(); + if ( 'lifterlms_page_llms-status' === $screen->id && isset( $_GET['tab'] ) && 'betas' === $_GET['tab'] ) { + $load = true; + } elseif ( 'lifterlms_page_llms-add-ons' === $screen->id ) { + $load = true; + } + + if ( ! $load ) { + return; + } + + wp_register_style( 'llms-helper', LLMS_HELPER_PLUGIN_URL . 'assets/css/llms-helper' . LLMS_ASSETS_SUFFIX . '.css', array(), LLMS_HELPER_VERSION ); + wp_enqueue_style( 'llms-helper' ); + + wp_style_add_data( 'llms-sl', 'rtl', 'replace' ); + wp_style_add_data( 'llms-sl', 'suffix', LLMS_ASSETS_SUFFIX ); + + } + +} +return new LLMS_Helper_Assets(); diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-betas.php b/libraries/lifterlms-helper/includes/class-llms-helper-betas.php new file mode 100644 index 0000000000..2395272650 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-betas.php @@ -0,0 +1,111 @@ +<?php +/** + * Handle status beta tab + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Betas + * + * @since 3.0.0 + */ +class LLMS_Helper_Betas { + + /** + * Constructor + * + * @since 3.0.0 + * + * @return void + */ + public function __construct() { + + add_filter( 'llms_admin_page_status_tabs', array( $this, 'add_tab' ) ); + + add_action( 'llms_before_admin_page_status', array( $this, 'output_tab' ) ); + + add_action( 'admin_init', array( $this, 'handle_form_submit' ) ); + + } + + /** + * Add the tab to the nav + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @param array $tabs Existing tabs. + * @return array + */ + public function add_tab( $tabs ) { + return llms_assoc_array_insert( $tabs, 'tools', 'betas', __( 'Beta Testing', 'lifterlms' ) ); + } + + /** + * Handle channel subscription saves + * + * @since 3.0.0 + * @since 3.2.0 Don't access `$_POST` directly. + * @since 3.2.1 Flush transient caches when a subscription changes. + * + * @return null|string Returns null when nonce errors or invalid data are submitted, otherwise returns an array of addon subscription data. + */ + public function handle_form_submit() { + + if ( ! llms_verify_nonce( '_llms_beta_sub_nonce', 'llms_save_channel_subscriptions' ) ) { + return; + } + + $subs = llms_filter_input( INPUT_POST, 'llms_channel_subscriptions', FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY ); + if ( ! $subs || ! is_array( $subs ) ) { + return; + } + + $new_subscription = false; + + foreach ( $subs as $id => $channel ) { + + $addon = llms_get_add_on( $id ); + if ( 'channel' !== $addon->get_channel_subscription() ) { + $addon->subscribe_to_channel( sanitize_text_field( $channel ) ); + $new_subscription = true; + } + } + + // When a channel subscription changes also flush caches so we'll get the most recent add-on data immediately and allow upgrading immediately from wp core update screens. + if ( $new_subscription ) { + llms_helper_flush_cache(); + } + + return $subs; + + } + + /** + * Output content for the beta testing screen + * + * @since 3.0.0 + * + * @param string $curr_tab Current status screen tab. + * @return void + */ + public function output_tab( $curr_tab ) { + + if ( 'betas' !== $curr_tab ) { + return; + } + + $addons = llms_helper_get_available_add_ons(); + array_unshift( $addons, 'lifterlms-com-lifterlms', 'lifterlms-com-lifterlms-helper' ); + include 'views/beta-testing.php'; + + } + +} +return new LLMS_Helper_Betas(); diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-cloned.php b/libraries/lifterlms-helper/includes/class-llms-helper-cloned.php new file mode 100644 index 0000000000..507d4cda32 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-cloned.php @@ -0,0 +1,68 @@ +<?php +/** + * Automatically attempt to activate already activated add-ons during clones + * + * @package LifterLMS_Helper/Classes + * + * @since 2.5.0 + * @version 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Cloned + * + * @since 2.5.0 + */ +class LLMS_Helper_Cloned { + + /** + * Constructor + * + * @since 2.5.0 + * + * @return void + */ + public function __construct() { + + add_action( 'llms_site_clone_detected', array( $this, 'handle_clone' ) ); + + } + + /** + * Attempt to automatically activate already activated add-ons when cloning + * + * If the key cannot be activated all activation related data will be removed + * Called when LifterLMS core detects a cloned site. + * + * @since 2.5.0 + * @since 3.0.0 Unknown. + * + * @return void + */ + public function handle_clone() { + + $keys = llms_helper_options()->get_license_keys(); + + if ( ! $keys ) { + return; + } + + $res = LLMS_Helper_Keys::activate_keys( array_keys( $keys ) ); + + if ( ! is_wp_error( $res ) ) { + + $data = $res['data']; + if ( isset( $data['activations'] ) ) { + foreach ( $data['activations'] as $activation ) { + LLMS_Helper_Keys::add_license_key( $activation ); + } + } + } + + } + +} + +return new LLMS_Helper_Cloned(); diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-install.php b/libraries/lifterlms-helper/includes/class-llms-helper-install.php new file mode 100644 index 0000000000..2421738030 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-install.php @@ -0,0 +1,174 @@ +<?php +/** + * Plugin installation + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Install + * + * @since 3.0.0 + */ +class LLMS_Helper_Install { + + /** + * Initialize the install class + * + * @since 3.0.0 + * @since 3.0.1 Unknown. + * + * @return void + */ + public static function init() { + add_action( 'admin_init', array( __CLASS__, 'check_version' ), 5 ); + } + + /** + * Checks the current LLMS version and runs installer if required + * + * @since 3.0.0 + * @since 3.4.0 Use llms_helper() in favor of deprecated LLMS_Helper(). + * + * @return void + */ + public static function check_version() { + + if ( ! defined( 'IFRAME_REQUEST' ) && get_option( 'llms_helper_version' ) !== llms_helper()->version ) { + + self::install(); + + /** + * Action run after the helper library is updated. + * + * @since 3.0.0 + */ + do_action( 'llms_helper_updated' ); + + } + } + + /** + * Core install function + * + * @since 3.0.0 + * @since 3.4.0 Skip migration when loaded as a library. + * + * @return void + */ + public static function install() { + + if ( ! is_blog_installed() ) { + return; + } + + do_action( 'llms_helper_before_install' ); + + if ( ( ! defined( 'LLMS_HELPER_LIB' ) || ! LLMS_HELPER_LIB ) && ! get_option( 'llms_helper_version', '' ) ) { + self::_migrate_300(); + } + + self::update_version(); + + do_action( 'llms_helper_after_install' ); + } + + /** + * Update the LifterLMS version record to the latest version + * + * @since 3.0.0 + * @since 3.4.0 Use llms_helper() in favor of deprecated LLMS_Helper(). + * + * @param string $version version number. + * @return void + */ + public static function update_version( $version = null ) { + delete_option( 'llms_helper_version' ); + add_option( 'llms_helper_version', is_null( $version ) ? llms_helper()->version : $version ); + } + + /** + * Migrate to version 3.0.0 + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * @since 3.4.0 Use core textdomain. + * + * @return void + */ + private static function _migrate_300() { + + $text = '<p><strong>' . __( 'Welcome to the LifterLMS Helper', 'lifterlms' ) . '</strong></p>'; + $text .= '<p>' . __( 'This plugin allows your website to interact with your subscriptions at LifterLMS.com to ensure your add-ons stay up to date.', 'lifterlms' ) . '</p>'; + // Translators: %1$s = Opening anchor tag; %2$s = closing anchor tag. + $text .= '<p>' . sprintf( __( 'You can activate your add-ons from the %1$sAdd-Ons & More%2$s screen.', 'lifterlms' ), '<a href="' . admin_url( 'admin.php?page=llms-add-ons' ) . '">', '</a>' ) . '</p>'; + + $keys = array(); + $addons = llms_get_add_ons(); + if ( ! is_wp_error( $addons ) && isset( $addons['items'] ) ) { + foreach ( $addons['items'] as $addon ) { + + $addon = llms_get_add_on( $addon ); + + if ( ! $addon->is_installable() ) { + continue; + } + + $option_name = sprintf( '%s_activation_key', $addon->get( 'slug' ) ); + + $key = get_option( $option_name ); + if ( $key ) { + $keys[] = get_option( $option_name ); + } + + delete_option( $option_name ); + delete_option( sprintf( '%s_update_key', $addon->get( 'slug' ) ) ); + + } + } + + if ( $keys ) { + + $res = LLMS_Helper_Keys::activate_keys( $keys ); + + if ( ! is_wp_error( $res ) ) { + + $data = $res['data']; + if ( isset( $data['activations'] ) ) { + + // Translators: %d = Number of keys that have been migrated. + $text .= '<p>' . sprintf( _n( '%d license has been automatically migrated from the previous version of the LifterLMS Helper', '%d licenses have been automatically migrated from the previous version of the LifterLMS Helper.', count( $data['activations'] ), 'lifterlms' ), count( $data['activations'] ) ) . ':</p>'; + + foreach ( $data['activations'] as $activation ) { + LLMS_Helper_Keys::add_license_key( $activation ); + $text .= '<p><em>' . $activation['license_key'] . '</em></p>'; + } + } + } + } + + LLMS_Admin_Notices::flash_notice( $text, 'info' ); + + // Clean up legacy options. + $remove = array( + 'lifterlms_stripe_activation_key', + 'lifterlms_paypal_activation_key', + 'lifterlms_gravityforms_activation_key', + 'lifterlms_mailchimp_activation_key', + 'llms_helper_key_migration', + ); + + foreach ( $remove as $opt ) { + delete_option( $opt ); + } + + } + +} + +LLMS_Helper_Install::init(); diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-keys.php b/libraries/lifterlms-helper/includes/class-llms-helper-keys.php new file mode 100644 index 0000000000..1f2914a94e --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-keys.php @@ -0,0 +1,230 @@ +<?php +/** + * License Key functions + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.4.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Keys + * + * @since 3.0.0 + */ +class LLMS_Helper_Keys { + + /** + * Activate LifterLMS License Keys with the remote server + * + * @since 3.0.0 + * @since 3.0.1 Unknown. + * + * @param string|array $keys Array or a white-space separated list of API keys. + * @return array + */ + public static function activate_keys( $keys ) { + + // Sanitize before sending. + if ( ! is_array( $keys ) ) { + $keys = explode( PHP_EOL, $keys ); + } + + $keys = array_map( 'sanitize_text_field', $keys ); + $keys = array_map( 'trim', $keys ); + $keys = array_unique( $keys ); + + $data = array( + 'keys' => $keys, + 'url' => get_site_url(), + ); + + $req = new LLMS_Dot_Com_API( '/license/activate', $data ); + return $req->get_result(); + + } + + /** + * Add a single license key + * + * @since 3.0.0 + * + * @param string $activation_data Array of activation details from api call. + * @return boolean True if option value has changed, false if not or if update failed. + */ + public static function add_license_key( $activation_data ) { + + $keys = llms_helper_options()->get_license_keys(); + $keys[ $activation_data['license_key'] ] = array( + 'product_id' => $activation_data['id'], + 'status' => 1, + 'license_key' => $activation_data['license_key'], + 'update_key' => $activation_data['update_key'], + 'addons' => $activation_data['addons'], + ); + + return llms_helper_options()->set_license_keys( $keys ); + + } + + /** + * Check all saved keys to ensure they're still active + * + * Outputs warnings if the key has expired or the status has changed remotely. + * + * Runs on daily cron (`llms_check_license_keys`). + * + * Only make api calls to check once / week. + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @param bool $force Ignore the once/week setting and force a check. + * @return void + */ + public static function check_keys( $force = false ) { + + // Don't trigger during AJAX Requests. + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + return; + } + + // Don't proceed if we don't have any keys to check. + $keys = llms_helper_options()->get_license_keys(); + if ( ! $keys ) { + return; + } + + if ( ! $force ) { + // Only check keys once a week. + $last_send = llms_helper_options()->get_last_keys_cron_check(); + if ( $last_send > apply_filters( 'llms_check_license_keys_interval', strtotime( '-1 week' ) ) ) { + return; + } + } + + // Record check time. + llms_helper_options()->set_last_keys_cron_check( time() ); + + $data = array( + 'keys' => array(), + 'url' => get_site_url(), + ); + + foreach ( $keys as $key ) { + $data['keys'][ $key['license_key'] ] = $key['update_key']; + } + + $req = new LLMS_Dot_Com_API( '/license/status', $data ); + if ( ! $req->is_error() ) { + + $res = $req->get_result(); + include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php'; + + /* Translators: %s = License Key */ + $msg = __( 'The license "%s" is no longer valid and was deactivated. Please visit your account dashboard at https://lifterlms.com/my-account for more information.', 'lifterlms' ); + + // Output error responses. + if ( isset( $res['data']['errors'] ) ) { + foreach ( array_keys( $res['data']['errors'] ) as $key ) { + self::remove_license_key( $key ); + LLMS_Admin_Notices::add_notice( + 'key_check_' . sanitize_text_field( $key ), + make_clickable( sprintf( $msg, $key ) ), + array( + 'type' => 'error', + 'dismiss_for_days' => 0, + ) + ); + } + } + + // Check status of keys, if the status has changed remove it locally. + if ( isset( $res['data']['keys'] ) ) { + foreach ( $res['data']['keys'] as $key => $data ) { + + if ( $data['status'] ) { + continue; + } + + self::remove_license_key( $key ); + LLMS_Admin_Notices::add_notice( + 'key_check_' . sanitize_text_field( $key ), + make_clickable( sprintf( $msg, $key ) ), + array( + 'type' => 'error', + 'dismiss_for_days' => 0, + ) + ); + + } + } + } + } + + /** + * Deactivate LifterLMS API keys with remote server + * + * @since 3.0.0 + * @since 3.4.1 Ensure key exists before attempting to deactivate it. + * + * @param array $keys Array of keys. + * @return array + */ + public static function deactivate_keys( $keys ) { + + $keys = array_map( 'sanitize_text_field', $keys ); + $keys = array_map( 'trim', $keys ); + + $data = array( + 'keys' => array(), + 'url' => get_site_url(), + ); + + $saved = llms_helper_options()->get_license_keys(); + foreach ( $keys as $key ) { + if ( isset( $saved[ $key ] ) && $saved[ $key ]['update_key'] ) { + $data['keys'][ $key ] = $saved[ $key ]['update_key']; + } + } + + $req = new LLMS_Dot_Com_API( '/license/deactivate', $data ); + return $req->get_result(); + + } + + /** + * Retrieve stored information about a key by the license key + * + * @since 3.3.1 + * + * @param string $key License key. + * @return array|false Associative array of license key information. Returns `false` if the provided license key was not found. + */ + public static function get( $key ) { + + $saved = llms_helper_options()->get_license_keys(); + return isset( $saved[ $key ] ) ? $saved[ $key ] : false; + + } + + /** + * Remove a single license key + * + * @since 3.0.0 + * + * @param string $key License key. + * @return boolean True if option value has changed, false if not or if update failed. + */ + public static function remove_license_key( $key ) { + $keys = llms_helper_options()->get_license_keys(); + if ( isset( $keys[ $key ] ) ) { + unset( $keys[ $key ] ); + } + return llms_helper_options()->set_license_keys( $keys ); + } + +} diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-options.php b/libraries/lifterlms-helper/includes/class-llms-helper-options.php new file mode 100644 index 0000000000..6a75a74b23 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-options.php @@ -0,0 +1,161 @@ +<?php +/** + * Get & Set Helper options + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.2.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Options + * + * @since 3.0.0 + * @since 3.2.0 Use `$instance` in favor of `$_instance`. + */ +class LLMS_Helper_Options { + + /** + * Singleton instance + * + * @var null|LLMS_Helper_Options + */ + protected static $instance = null; + + /** + * Main Instance + * + * @since 3.0.0 + * @since 3.2.0 Use `self::$instance` in favor of `self::$_instance`. + * + * @return LLMS_Helper_Options + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Retrive a single option + * + * @since 3.0.0 + * + * @param string $key Option name. + * @param mixed $default Default option value if option isn't already set. + * @return mixed + */ + private function get_option( $key, $default = '' ) { + + $options = $this->get_options(); + + if ( isset( $options[ $key ] ) ) { + return $options[ $key ]; + } + + return $default; + + } + + /** + * Retrieve all upgrader options array + * + * @since 3.0.0 + * + * @return array + */ + private function get_options() { + return get_option( 'llms_helper_options', array() ); + } + + /** + * Update the value of an option + * + * @since 3.0.0 + * + * @param string $key Option name. + * @param mixed $val Option value. + * @return boolean True if option value has changed, false if not or if update failed. + */ + private function set_option( $key, $val ) { + + $options = $this->get_options(); + $options[ $key ] = $val; + return update_option( 'llms_helper_options', $options, false ); + + } + + /** + * Get info about addon channel subscriptions + * + * @since 3.0.0 + * + * @return array + */ + public function get_channels() { + return $this->get_option( 'channels', array() ); + } + + /** + * Set info about addon channel subscriptions + * + * @since 3.0.0 + * + * @param array $channels Array of channel information. + * @return boolean True if option value has changed, false if not or if update failed. + */ + public function set_channels( $channels ) { + return $this->set_option( 'channels', $channels ); + } + + /** + * Retrieve a timestamp for the last time the keys check cron was run + * + * @since 3.0.0 + * + * @return int + */ + public function get_last_keys_cron_check() { + return $this->get_option( 'last_keys_cron_check', 0 ); + } + + /** + * Set the last cron check time + * + * @since 3.0.0 + * + * @param int $time Timestamp. + * @return boolean True if option value has changed, false if not or if update failed. + */ + public function set_last_keys_cron_check( $time ) { + return $this->set_option( 'last_keys_cron_check', $time ); + } + + /** + * Retrieve saved license key data + * + * @since 3.0.0 + * + * @return array + */ + public function get_license_keys() { + return $this->get_option( 'license_keys', array() ); + } + + /** + * Update saved license key data + * + * @since 3.0.0 + * + * @param array $keys Key data to save. + * @return boolean True if option value has changed, false if not or if update failed. + */ + public function set_license_keys( $keys ) { + return $this->set_option( 'license_keys', $keys ); + } + +} diff --git a/libraries/lifterlms-helper/includes/class-llms-helper-upgrader.php b/libraries/lifterlms-helper/includes/class-llms-helper-upgrader.php new file mode 100644 index 0000000000..855e9cc317 --- /dev/null +++ b/libraries/lifterlms-helper/includes/class-llms-helper-upgrader.php @@ -0,0 +1,511 @@ +<?php +/** + * Actions and LifterLMS.com API interactions related to plugin and theme updates for LifterLMS premium add-ons + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Upgrader + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * @since 3.1.0 Load changelogs from the make blog in favor of static html changelogs. + */ +class LLMS_Helper_Upgrader { + + /** + * Singleton instance + * + * @var null|LLMS_Helper_Upgrader + */ + protected static $instance = null; + + /** + * Main Instance of LLMS_Helper_Upgrader + * + * @since 3.0.0 + * @since version] Use `self::$instance` in favor of `self::$_instance`. + * + * @return LLMS_Helper_Upgrader + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Constructor + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * + * @return void + */ + private function __construct() { + + // Setup a llms add-on plugin info. + add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 ); + + // Authenticate and get a real download link during add-on upgrade attempts. + add_filter( 'upgrader_package_options', array( $this, 'upgrader_package_options' ) ); + + // Add llms add-on info to list of available updates. + add_filter( 'pre_set_site_transient_update_themes', array( $this, 'pre_set_site_transient_update_things' ) ); + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'pre_set_site_transient_update_things' ) ); + + $products = llms_get_add_ons(); + if ( ! is_wp_error( $products ) && isset( $products['items'] ) ) { + foreach ( (array) $products['items'] as $product ) { + + if ( 'plugin' === $product['type'] && $product['update_file'] ) { + add_action( "in_plugin_update_message-{$product['update_file']}", array( $this, 'in_plugin_update_message' ), 10, 2 ); + } + } + } + + } + + /** + * Install an add-on from LifterLMS.com + * + * @since 3.0.0 + * @since 3.2.0 Use strict comparison for `in_array()`. + * @since 3.4.0 Use core textdomain. + * + * @param string|obj $addon_or_id ID for the add-on or an instance of the LLMS_Add_On. + * @param string $action Installation type [install|update]. + * @return WP_Error|true + */ + public function install_addon( $addon_or_id, $action = 'install' ) { + + // Setup the addon. + $addon = is_a( $addon_or_id, 'LLMS_Add_On' ) ? $addon_or_id : llms_get_add_on( $addon_or_id ); + if ( ! $addon ) { + return new WP_Error( 'invalid_addon', __( 'Invalid add-on ID.', 'lifterlms' ) ); + } + + if ( ! in_array( $action, array( 'install', 'update' ), true ) ) { + return new WP_Error( 'invalid_action', __( 'Invalid action.', 'lifterlms' ) ); + } + + if ( ! $addon->is_installable() ) { + return new WP_Error( 'not_installable', __( 'Add-on cannot be installable.', 'lifterlms' ) ); + } + + // Make sure it's not already installed. + if ( 'install' === $action && $addon->is_installed() ) { + // Translators: %s = Add-on name. + return new WP_Error( 'installed', sprintf( __( '%s is already installed', 'lifterlms' ), $addon->get( 'title' ) ) ); + } + + // Get download info via llms.com api. + $dl_info = $addon->get_download_info(); + if ( is_wp_error( $dl_info ) ) { + return $dl_info; + } + if ( ! isset( $dl_info['data']['url'] ) ) { + return new WP_Error( 'no_url', __( 'An error occured while attempting to retrieve add-on download information. Please try again.', 'lifterlms' ) ); + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin(); + + if ( 'plugin' === $addon->get_type() ) { + + $upgrader = new Plugin_Upgrader( $skin ); + + } elseif ( 'theme' === $addon->get_type() ) { + + $upgrader = new Theme_Upgrader( $skin ); + + } else { + + return new WP_Error( 'inconceivable', __( 'The requested action is not possible.', 'lifterlms' ) ); + + } + + if ( 'install' === $action ) { + remove_filter( 'upgrader_package_options', array( $this, 'upgrader_package_options' ) ); + $result = $upgrader->install( $dl_info['data']['url'] ); + add_filter( 'upgrader_package_options', array( $this, 'upgrader_package_options' ) ); + } elseif ( 'update' === $action ) { + $result = $upgrader->upgrade( $addon->get( 'update_file' ) ); + } + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( is_wp_error( $skin->result ) ) { + return $skin->result; + } elseif ( is_null( $result ) ) { + return new WP_Error( 'filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Output additional information on plugins update screen when updates are available for an unlicensed addon + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * @since 3.4.0 Use core textdomain. + * + * @param array $plugin_data Array of plugin data. + * @param array $res Response data. + * @return void + */ + public function in_plugin_update_message( $plugin_data, $res ) { + + if ( empty( $plugin_data['package'] ) ) { + + echo '<style>p.llms-msg:before { content: ""; }</style>'; + + echo '<p class="llms-msg"><strong>'; + _e( 'Your LifterLMS add-on is currently unlicensed and cannot be updated!', 'lifterlms' ); + echo '</strong></p>'; + + echo '<p class="llms-msg">'; + // Translators: %1$s = Opening anchor tag; %2$s = Closing anchor tag. + printf( __( 'If you already have a license, you can activate it on the %1$sadd-ons management screen%2$s.', 'lifterlms' ), '<a href="' . esc_url( admin_url( 'admin.php?page=llms-add-ons' ) ) . '">', '</a>' ); + echo '</p>'; + + echo '<p class="llms-msg">'; + // Translators: %s = URI to licensing FAQ. + printf( __( 'Learn more about LifterLMS add-on licensing at %s.', 'lifterlms' ), make_clickable( 'https://lifterlms.com/docs/lifterlms-helper/' ) ); + echo '</p><p style="display:none;">'; + + } + + } + + /** + * Filter API calls to get plugin information and replace it with data from LifterLMS.com API for our addons + * + * @since 3.0.0 + * + * @param bool $response False (denotes API call should be made to wp.org for plugin info). + * @param string $action Name of the API action. + * @param obj $args Additional API call args. + * @return false|obj + */ + public function plugins_api( $response, $action = '', $args = null ) { + + if ( 'plugin_information' !== $action ) { + return $response; + } + + if ( empty( $args->slug ) ) { + return $response; + } + + $core = false; + + if ( 'lifterlms' === $args->slug ) { + remove_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 ); + $args->slug = 'lifterlms-com-lifterlms'; + $core = true; + } + + if ( 0 !== strpos( $args->slug, 'lifterlms-com-' ) ) { + return $response; + } + + $response = $this->set_plugins_api( $args->slug, true ); + + if ( $core ) { + add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 ); + } + + return $response; + + } + + /** + * Handle setting the site transient for plugin updates + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * + * @param obj $value Transient value. + * @return obj + */ + public function pre_set_site_transient_update_things( $value ) { + + if ( empty( $value ) ) { + return $value; + } + + $which = current_filter(); + if ( 'pre_set_site_transient_update_plugins' === $which ) { + $type = 'plugin'; + } elseif ( 'pre_set_site_transient_update_themes' === $which ) { + $type = 'theme'; + } else { + return $value; + } + + $all_products = llms_get_add_ons( false ); + if ( is_wp_error( $all_products ) || ! isset( $all_products['items'] ) ) { + return $value; + } + + foreach ( $all_products['items'] as $addon_data ) { + + $addon = llms_get_add_on( $addon_data ); + + if ( ! $addon->is_installable() || ! $addon->is_installed() ) { + continue; + } + + if ( $type !== $addon->get_type() ) { + continue; + } + + $file = $addon->get( 'update_file' ); + + if ( 'plugin' === $type ) { + + if ( 'lifterlms-com-lifterlms' === $addon->get( 'id' ) ) { + if ( 'stable' === $addon->get_channel_subscription() || ! $addon->get( 'version_beta' ) ) { + continue; + } + } + + $item = (object) $this->set_plugins_api( $addon->get( 'id' ), false ); + + } elseif ( 'theme' === $type ) { + + $item = array( + 'theme' => $file, + 'new_version' => $addon->get_latest_version(), + 'url' => $addon->get_permalink(), + 'package' => true, + ); + } + + if ( $addon->has_available_update() ) { + + $value->response[ $file ] = $item; + unset( $value->no_update[ $file ] ); + + } else { + + $value->no_update[ $file ] = $item; + unset( $value->response[ $file ] ); + + } + } + + return $value; + + } + + /** + * Setup an object of addon data for use when requesting plugin information normally acquired from wp.org + * + * @since 3.0.0 + * @since 3.2.1 Set package to `true` for add-ons which don't require a license. + * + * @param string $id Addon id. + * @param bool $include_sections Whether or not to include additional sections like the description and changelog. + * @return object + */ + private function set_plugins_api( $id, $include_sections = true ) { + + $addon = llms_get_add_on( $id ); + + if ( 'lifterlms-com-lifterlms' === $id && false !== strpos( $addon->get_latest_version(), 'beta' ) ) { + + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + $item = plugins_api( + 'plugin_information', + array( + 'slug' => 'lifterlms', + 'fields' => array( + 'banners' => true, + 'icons' => true, + ), + ) + ); + $item->version = $addon->get_latest_version(); + $item->new_version = $addon->get_latest_version(); + $item->package = true; + + unset( $item->versions ); + + $item->sections['changelog'] = $this->get_changelog_for_api( $addon ); + + return $item; + + } + + $item = array( + 'name' => $addon->get( 'title' ), + 'slug' => $id, + 'version' => $addon->get_latest_version(), + 'new_version' => $addon->get_latest_version(), + 'author' => '<a href="https://lifterlms.com/">' . $addon->get( 'author' )['name'] . '</a>', + 'author_profile' => $addon->get( 'author' )['link'], + 'requires' => $addon->get( 'version_wp' ), + 'tested' => '', + 'requires_php' => $addon->get( 'version_php' ), + 'compatibility' => '', + 'homepage' => $addon->get( 'permalink' ), + 'download_link' => '', + 'package' => ( $addon->is_licensed() || ! $addon->requires_license() ), + 'banners' => array( + 'low' => $addon->get( 'image' ), + ), + ); + + if ( $include_sections ) { + + $item['sections'] = array( + 'description' => $addon->get( 'description' ), + 'changelog' => $this->get_changelog_for_api( $addon ), + ); + + } + + return (object) $item; + + } + + /** + * Retrieve the changelog for an addon + * + * Attempts to retrieve changelog HTML from the make blog. + * + * If the add-on's changelog is empty or a static html file, returns an error + * with a link to the release notes category on the make blog. + * + * @since 3.0.0 + * @since 3.1.0 Retrieve changelog from the make blog in favor of legacy static html changelogs. + * @since 3.2.0 Fix usage of incorrect textdomain. + * + * @param LLMS_Add_On $addon Add-on object. + * @return string + */ + private function get_changelog_for_api( $addon ) { + + $src = $addon->get( 'changelog' ); + $split = array_filter( explode( '/', $src ) ); + $tag = end( $split ); + + $logs = false; + if ( ! empty( $tag ) && false === strpos( $tag, '.html' ) ) { + $logs = $this->get_changelog_html( $tag, $src ); + } + + // Translators: %s = URL for the changelog website. + return $logs ? $logs : make_clickable( sprintf( __( 'There was an error retrieving the changelog.<br>Try visiting %s for recent changelogs.', 'lifterlms' ), 'https://make.lifterlms.com/category/release-notes/' ) ); + + } + + /** + * Retrieve changelog information from the make blog + * + * Retrieves the most recent 10 changelog entries for the add-on, formats the returned information + * into a format suitable to display within the thickbox, adds a link to the full changelog, + * and returns the html string. + * + * If an error is encountered, returns an empty string. + * + * @since 3.1.0 + * @since 3.2.0 Fix usage of incorrect textdomain. + * + * @param string $tag Tag slug for the add-on on the blog. + * @param string $url Full URL to the changelog entries for the add-on. + * @return string + */ + private function get_changelog_html( $tag, $url ) { + + $ret = ''; + $req = wp_remote_get( add_query_arg( 'slug', $tag, 'https://make.lifterlms.com/wp-json/wp/v2/tags' ) ); + $body = json_decode( wp_remote_retrieve_body( $req ), true ); + + if ( ! empty( $body ) && ! empty( $body[0]['_links']['wp:post_type'][0]['href'] ) ) { + + $logs_url = $body[0]['_links']['wp:post_type'][0]['href']; + $logs_req = wp_remote_get( $logs_url ); + $logs = json_decode( wp_remote_retrieve_body( $logs_req ), true ); + + if ( ! empty( $logs ) && is_array( $logs ) ) { + foreach ( $logs as $log ) { + $ts = strtotime( $log['date_gmt'] ); + $date = function_exists( 'wp_date' ) ? wp_date( 'Y-m-d', $ts ) : gmdate( 'Y-m-d', $ts ); + $split = array_filter( explode( ' ', $log['title']['rendered'] ) ); + $ver = end( $split ); + // Translators: %1$s - Version number; %2$s - Release date. + $ret .= '<h4>' . sprintf( __( 'Version %1$s - %2$s', 'lifterlms' ), sanitize_text_field( wp_strip_all_tags( trim( $ver ) ) ), $date ) . '</h4>'; + $ret .= strip_tags( $log['content']['rendered'], '<ul><li><p><a><b><strong><em><i>' ); + } + } + + $ret .= '<br>'; + // Translators: %s = URL to the full changelog. + $ret .= '<p>' . make_clickable( sprintf( __( 'View the full changelog at %s.', 'lifterlms' ), $url ) ) . '</p>'; + + } + + return $ret; + + } + + /** + * Get a real package download url for a LifterLMS add-on + * + * This is called immediately prior to package upgrades. + * + * @since 3.0.0 + * @since 3.0.2 Unknown. + * @since 3.2.1 Correctly process addons which do not require a license (e.g. free products). + * + * @param array $options Package option data. + * @return array + */ + public function upgrader_package_options( $options ) { + + if ( ! isset( $options['hook_extra'] ) ) { + return $options; + } + + if ( isset( $options['hook_extra']['plugin'] ) ) { + $file = $options['hook_extra']['plugin']; + } elseif ( isset( $options['hook_extra']['theme'] ) ) { + $file = $options['hook_extra']['theme']; + } else { + return $options; + } + + $addon = llms_get_add_on( $file, 'update_file' ); + if ( ! $addon || ! $addon->is_installable() || ( $addon->requires_license() && ! $addon->is_licensed() ) ) { + return $options; + } + + $info = $addon->get_download_info(); + if ( is_wp_error( $info ) || ! isset( $info['data'] ) || ! isset( $info['data']['url'] ) ) { + return $options; + } + + if ( true === $options['package'] ) { + $options['package'] = $info['data']['url']; + } + + return $options; + + } + +} diff --git a/libraries/lifterlms-helper/includes/functions-llms-helper.php b/libraries/lifterlms-helper/includes/functions-llms-helper.php new file mode 100644 index 0000000000..2d43ae31af --- /dev/null +++ b/libraries/lifterlms-helper/includes/functions-llms-helper.php @@ -0,0 +1,61 @@ +<?php +/** + * Helper functions + * + * @package LifterLMS_Helper/Functions + * + * @since 2.2.0 + * @version 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Retrieve the LLMS_Helper_Options singleton + * + * @since 3.0.0 + * + * @return LLMS_Helper_Options + */ +function llms_helper_options() { + return LLMS_Helper_Options::instance(); +} + +/** + * Retrieve an array of addons that are available via currently active License Keys + * + * @since 3.0.0 + * + * @param bool $installable_only If true, only includes installable addons, if false, includes non-installable addons (like bundles). + * @return array + */ +function llms_helper_get_available_add_ons( $installable_only = true ) { + + $ids = array(); + foreach ( llms_helper_options()->get_license_keys() as $key ) { + if ( 1 == $key['status'] ) { + $ids = array_merge( $ids, $key['addons'] ); + } + if ( false === $installable_only ) { + $ids[] = $key['product_id']; + } + } + + return array_unique( $ids ); + +} + +/** + * Deletes transient data related to plugin and theme updates + * + * @since 3.2.1 + * + * @return void + */ +function llms_helper_flush_cache() { + + delete_transient( 'llms_products_api_result' ); + delete_site_transient( 'update_plugins' ); + delete_site_transient( 'update_themes' ); + +} diff --git a/libraries/lifterlms-helper/includes/model-llms-helper-add-on.php b/libraries/lifterlms-helper/includes/model-llms-helper-add-on.php new file mode 100644 index 0000000000..a45a23ab46 --- /dev/null +++ b/libraries/lifterlms-helper/includes/model-llms-helper-add-on.php @@ -0,0 +1,18 @@ +<?php +/** + * Extends core class to allow interaction with the .com api + * + * @package LifterLMS_Helper/Classes + * + * @since 3.0.0 + * @version 3.2.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Deprecated file. + * + * @deprecated 3.2.0 File `includes/model-llms-helper-add-on.php` is deprecated, use `includes/models/class-llms-helper-add-on.php` instead. + */ +_deprecated_file( __FILE__, '3.2.0', LLMS_HELPER_PLUGIN_DIR . 'includes/models/class-llms-helper-add-on.php' ); diff --git a/libraries/lifterlms-helper/includes/models/class-llms-helper-add-on.php b/libraries/lifterlms-helper/includes/models/class-llms-helper-add-on.php new file mode 100644 index 0000000000..866468d77a --- /dev/null +++ b/libraries/lifterlms-helper/includes/models/class-llms-helper-add-on.php @@ -0,0 +1,259 @@ +<?php +/** + * Extends core class to allow interaction with the .com api + * + * @package LifterLMS_Helper/Models + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_Helper_Add_On + * + * @since 3.0.0 + * @since 3.2.0 Moved from `includes/model-llms-helper-add-on.php`. + */ +class LLMS_Helper_Add_On extends LLMS_Add_On { + + /** + * Find a license key for the add-on + * + * @since 3.0.0 + * @since 3.2.0 Use strict comparison for `in_array()`. + * @since 3.2.1 Use `requires_license()` rather than checking the add-on's `has_license` prop directly. + * + * @return string|false + */ + public function find_license() { + + /** + * If the addon doesn't require a license return the first found license to ensure + * that the core can be updated via a license when subscribed to a beta channel + * and that the helper can always be upgraded. + */ + $requires_license = $this->requires_license(); + + $id = $this->get( 'id' ); + foreach ( llms_helper_options()->get_license_keys() as $data ) { + /** + * 1. If license is not required, return the first license found. + * 2. If the addon matches the licensed product + * 3. If the addon is included in the licensed bundle product. + */ + if ( ! $requires_license || $id === $data['product_id'] || in_array( $id, $data['addons'], true ) ) { + return $data; + } + } + + return false; + + } + + /** + * Retrieve the update channel for the addon + * + * @since 3.0.0 + * + * @return string + */ + public function get_channel_subscription() { + $channels = llms_helper_options()->get_channels(); + return isset( $channels[ $this->get( 'id' ) ] ) ? $channels[ $this->get( 'id' ) ] : 'stable'; + } + + /** + * Retrieve download information for an add-on + * + * @since 3.0.0 + * @since 3.2.1 Allow getting download info for add-ons which do not require licenses. + * @since 3.4.0 Use core textdomain. + * + * @return WP_Error|array + */ + public function get_download_info() { + + $key = $this->find_license(); + + if ( $this->requires_license() && ! $key ) { + return new WP_Error( 'no_license', __( 'Unable to locate a license key for the selected add-on.', 'lifterlms' ) ); + } + + $args = array( + 'url' => get_site_url(), + 'add_on_slug' => $this->get( 'slug' ), + 'channel' => $this->get_channel_subscription(), + ); + + if ( $key ) { + $args['license_key'] = $key['license_key']; + $args['update_key'] = $key['update_key']; + } + + $req = new LLMS_Dot_Com_API( + '/license/download', + $args + ); + + $data = $req->get_result(); + + if ( $req->is_error() ) { + return $data; + } + + return $data; + + } + + /** + * Translate strings + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @param string $string Untranslated string / key. + * @return string + */ + public function get_l10n( $string ) { + + $strings = array( + + 'active' => __( 'Active', 'lifterlms' ), + 'inactive' => __( 'Inactive', 'lifterlms' ), + + 'installed' => __( 'Installed', 'lifterlms' ), + 'uninstalled' => __( 'Not Installed', 'lifterlms' ), + + 'activate' => __( 'Activate', 'lifterlms' ), + 'deactivate' => __( 'Deactivate', 'lifterlms' ), + 'install' => __( 'Install', 'lifterlms' ), + + 'none' => __( 'N/A', 'lifterlms' ), + + 'license_active' => __( 'Licensed', 'lifterlms' ), + 'license_inactive' => __( 'Unlicensed', 'lifterlms' ), + + ); + + return $strings[ $string ]; + + } + + /** + * Determine the status of an addon's license + * + * @since 3.0.0 + * @since 3.2.1 Use `requires_license()` instead of checking `has_license` prop directly. + * + * @param bool $translate If true, returns the translated string for on-screen display. + * @return string + */ + public function get_license_status( $translate = false ) { + + if ( ! $this->requires_license() ) { + $ret = 'none'; + } else { + $ret = $this->is_licensed() ? 'license_active' : 'license_inactive'; + } + + return $translate ? $this->get_l10n( $ret ) : $ret; + + } + + /** + * Install the add-on via LifterLMS.com + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @return string|WP_Error + */ + public function install() { + + $ret = LLMS_Helper()->upgrader()->install_addon( $this ); + + if ( true === $ret ) { + + /* Translators: %s = Add-on name */ + return sprintf( __( '%s was successfully installed.', 'lifterlms' ), $this->get( 'title' ) ); + + } elseif ( is_wp_error( $ret ) ) { + + return $ret; + + } + + /* Translators: %s = Add-on name */ + return new WP_Error( 'activation', sprintf( __( 'Could not install %s.', 'lifterlms' ), $this->get( 'title' ) ) ); + + } + + /** + * Determines if the add-on is licensed + * + * @since 3.0.0 + * + * @return bool + */ + public function is_licensed() { + return ( false !== $this->find_license() ); + } + + /** + * Determines if the add-on requires a license + * + * @since 3.2.1 + * + * @return bool + */ + public function requires_license() { + return llms_parse_bool( $this->get( 'has_license' ) ); + } + + /** + * Update the addons update channel subscription + * + * @since 3.0.0 + * + * @param string $channel Channel name [stable|beta]. + * @return boolean + */ + public function subscribe_to_channel( $channel = 'stable' ) { + + $channels = llms_helper_options()->get_channels(); + $channels[ $this->get( 'id' ) ] = $channel; + return llms_helper_options()->set_channels( $channels ); + + } + + /** + * Install the add-on via LifterLMS.com + * + * @since 3.0.0 + * @since 3.4.0 Use core textdomain. + * + * @return string|WP_Error + */ + public function update() { + + $ret = LLMS_Helper()->upgrader()->install_addon( $this, 'update' ); + + if ( true === $ret ) { + + /* Translators: %s = Add-on name */ + return sprintf( __( '%s was successfully updated.', 'lifterlms' ), $this->get( 'title' ) ); + + } elseif ( is_wp_error( $ret ) ) { + + return $ret; + + } + + /* Translators: %s = Add-on name */ + return new WP_Error( 'activation', sprintf( __( 'Could not update %s.', 'lifterlms' ), $this->get( 'title' ) ) ); + + } + +} diff --git a/libraries/lifterlms-helper/includes/models/index.php b/libraries/lifterlms-helper/includes/models/index.php new file mode 100644 index 0000000000..ff2b6071fd --- /dev/null +++ b/libraries/lifterlms-helper/includes/models/index.php @@ -0,0 +1 @@ +<?php // Shhh. diff --git a/libraries/lifterlms-helper/includes/views/beta-testing.php b/libraries/lifterlms-helper/includes/views/beta-testing.php new file mode 100644 index 0000000000..c418620ffb --- /dev/null +++ b/libraries/lifterlms-helper/includes/views/beta-testing.php @@ -0,0 +1,97 @@ +<?php +/** + * View for displaying the Beta Testing tab on the "Status" screen + * + * @package LifterLMS_Helper/Views + * + * @since 3.0.0 + * @version 3.4.0 + */ + +defined( 'ABSPATH' ) || exit; +?> +<form action="" class="llms-beta-main" method="POST"> + + <aside class="llms-beta-aside"> + + <h1><?php _e( 'Beta Testing Warnings and FAQs', 'lifterlms' ); ?></h1> + + <h3><?php _e( 'Always test with caution!', 'lifterlms' ); ?></h3> + <p><strong><?php _e( 'Beta releases may not be stable. We may not be able to fix issues caused by using a beta release. We urge you to only use beta versions in testing environments!', 'lifterlms' ); ?></strong></p> + <p><?php _e( 'Subscribing to the <em>beta channel</em> for LifterLMS or any available add-ons will allow you to automatically update to the latest beta release for the given plugin or theme.', 'lifterlms' ); ?></p> + <p><?php _e( 'When no beta versions are available, automatic updates will be to the latest stable version of the plugin or theme.', 'lifterlms' ); ?></p> + + <h3><?php _e( 'Rolling back and restoring data', 'lifterlms' ); ?></h3> + <p><strong><?php _e( 'This plugin does not provide you with the ability to rollback from a beta version.', 'lifterlms' ); ?></strong></p> + <p><?php _e( 'To rollback you should subscribe to the stable channel, delete the beta version of the plugin, and then re-install the latest version. If a database migration was run you should also restore your database from a backup.', 'lifterlms' ); ?></p> + + <h3><?php _e( 'Reporting bugs and contributing', 'lifterlms' ); ?></h3> + <p> + <?php + // Translators: %1$s = Opening anchor link; %2$s = closing anchor link. + printf( __( 'We welcome contributions of all kinds, review our contribution guidelines on %1$sGitHub%2$s to get started.', 'lifterlms' ), '<a href="https://github.com/gocodebox/lifterlms/blob/master/.github/CONTRIBUTING.md">', '</a>' ); + ?> + </p> + <p> + <?php + // Translators: %s = Link to bug report. + printf( __( 'If you encounter a bug while beta testing, please report it at %s.', 'lifterlms' ), make_clickable( 'https://github.com/gocodebox/lifterlms/issues' ) ); + ?> + </p> + + <h3><?php _e( 'Still have questions?', 'lifterlms' ); ?></h3> + <p> + <?php + // Translators: %s = Link to guide. + printf( __( 'Check out our Guide to Beta Testing at %s.', 'lifterlms' ), make_clickable( 'https://lifterlms.com/docs/beta-testing/' ) ); + ?> + </p> + + </aside> + + <table class="llms-table zebra text-left size-large llms-beta-table"> + <thead> + <tr> + <th><?php _e( 'Name', 'lifterlms' ); ?></th> + <th><?php _e( 'Channel', 'lifterlms' ); ?></th> + <th><?php _e( 'Installed Version', 'lifterlms' ); ?></th> + <th><?php _e( 'Beta Version', 'lifterlms' ); ?></th> + </tr> + </thead> + <tbody> + <?php + foreach ( $addons as $addon ) : + $addon = llms_get_add_on( $addon ); + ?> + <tr> + <td><?php echo $addon->get( 'title' ); ?></td> + <td> + <select name="llms_channel_subscriptions[<?php echo $addon->get( 'id' ); ?>]"> + <option value="stable" <?php selected( 'stable', $addon->get_channel_subscription() ); ?>><?php _e( 'Stable', 'lifterlms' ); ?></option> + <option value="beta" <?php selected( 'beta', $addon->get_channel_subscription() ); ?>><?php _e( 'Beta', 'lifterlms' ); ?></option> + </select> + </td> + <td><?php echo $addon->get_installed_version(); ?></td> + <td><?php echo $addon->get( 'version_beta' ) ? $addon->get( 'version_beta' ) : __( 'N/A', 'lifterlms' ); ?></td> + </tr> + <?php endforeach; ?> + </tbody> + <tfoot> + <tr> + <th colspan="4"><button class="llms-button-primary" id="llms-channel-submit" type="submit"><?php _e( 'Save & Update', 'lifterlms' ); ?></button></th> + </tr> + </tfoot> + </table> + + <script> + document.getElementById( 'llms-channel-submit' ).onclick = function( e ) { + if ( ! window.confirm( "<?php esc_attr_e( 'Are you sure you want to enable or disable beta testing for these plugins and themes?', 'lifterlms' ); ?>" ) ) { + e.preventDefault(); + } + } + </script> + + <?php wp_nonce_field( 'llms_save_channel_subscriptions', '_llms_beta_sub_nonce' ); ?> + +</form> +<?php diff --git a/libraries/lifterlms-helper/includes/views/index.php b/libraries/lifterlms-helper/includes/views/index.php new file mode 100644 index 0000000000..81b35c2f9d --- /dev/null +++ b/libraries/lifterlms-helper/includes/views/index.php @@ -0,0 +1 @@ +<?php // Shh. diff --git a/libraries/lifterlms-helper/lifterlms-helper.php b/libraries/lifterlms-helper/lifterlms-helper.php new file mode 100644 index 0000000000..3c4a7c6e45 --- /dev/null +++ b/libraries/lifterlms-helper/lifterlms-helper.php @@ -0,0 +1,65 @@ +<?php +/** + * LifterLMS Helper main plugin file + * + * @package LifterLMS_Helper/Main + * + * @since 1.0.0 + * @version 3.3.0 + * + * Plugin Name: LifterLMS Helper + * Plugin URI: https://lifterlms.com/ + * Description: Update, install, and beta test LifterLMS and LifterLMS add-ons + * Version: 3.4.1 + * Author: LifterLMS + * Author URI: https://lifterlms.com + * Text Domain: lifterlms + * Domain Path: /i18n + * License: GPLv3 + * License URI: https://www.gnu.org/licenses/gpl-3.0.html + * Requires LifterLMS: 3.22.0 + */ + +defined( 'ABSPATH' ) || exit; + +// Allow the helper to be disabled via constant when loaded as a library within the LifterLMS core. +if ( defined( 'LLMS_HELPER_LIB' ) && defined( 'LLMS_HELPER_DISABLE' ) && LLMS_HELPER_DISABLE ) { + return; +} + +if ( ! defined( 'LLMS_HELPER_PLUGIN_FILE' ) ) { + define( 'LLMS_HELPER_PLUGIN_FILE', __FILE__ ); +} + +if ( ! defined( 'LLMS_HELPER_PLUGIN_DIR' ) ) { + define( 'LLMS_HELPER_PLUGIN_DIR', dirname( __FILE__ ) . '/' ); +} + +if ( ! defined( 'LLMS_HELPER_PLUGIN_URL' ) ) { + define( 'LLMS_HELPER_PLUGIN_URL', trailingslashit( plugin_dir_url( __FILE__ ) ) ); +} + +if ( ! class_exists( 'LifterLMS_Helper' ) ) { + + require_once LLMS_HELPER_PLUGIN_DIR . 'class-lifterlms-helper.php'; + + /** + * Returns the main instance of the LifterLMS_Helper class + * + * @since 3.2.0 + * + * @return LifterLMS_Helper + */ + function llms_helper() { + return LifterLMS_Helper::instance(); + } +} + +/** + * Allow usage of the deprecated `LLMS_Helper()` function. + * + * @deprecated 3.2.0 Function `LLMS_Helper()` is deprecated in favor of `llms_helper()`. + */ +use function LLMS_Helper as llms_helper; + +return llms_helper(); diff --git a/libraries/lifterlms-rest/class-lifterlms-rest-api.php b/libraries/lifterlms-rest/class-lifterlms-rest-api.php new file mode 100644 index 0000000000..5faf440e57 --- /dev/null +++ b/libraries/lifterlms-rest/class-lifterlms-rest-api.php @@ -0,0 +1,269 @@ +<?php +/** + * LifterLMS_REST_API main class. + * + * @package LifterLMS_REST_API/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.18 + */ + +defined( 'ABSPATH' ) || exit; + +require_once LLMS_REST_API_PLUGIN_DIR . 'includes/traits/class-llms-rest-trait-singleton.php'; + +/** + * LifterLMS_REST_API class. + * + * @since 1.0.0-beta.1 + */ +final class LifterLMS_REST_API { + + use LLMS_REST_Trait_Singleton; + + /** + * Current version of the plugin. + * + * @var string + */ + public $version = '1.0.0-beta.21'; + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Load authentication early. + * @since 1.0.0-beta.17 Only localize when loaded as an independent plugin. + * + * @return void + */ + private function __construct() { + + if ( ! defined( 'LLMS_REST_API_VERSION' ) ) { + define( 'LLMS_REST_API_VERSION', $this->version ); + } + + /** + * When loaded as a library included by the LifterLMS core localization is handled by the LifterLMS core. + * + * When the plugin is loaded by itself as a plugin, we must localize it independently. + */ + if ( ! defined( 'LLMS_REST_API_LIB' ) || ! LLMS_REST_API_LIB ) { + add_action( 'init', array( $this, 'load_textdomain' ), 0 ); + } + + // Authentication needs to run early to handle basic auth. + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-authentication.php'; + + // Load everything else. + add_action( 'plugins_loaded', array( $this, 'init' ), 10 ); + + } + + /** + * Include files and instantiate classes. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Load authentication early. + * + * @return void + */ + public function includes() { + + // Abstracts. + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/abstracts/class-llms-rest-database-resource.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/abstracts/class-llms-rest-webhook-data.php'; + + // Functions. + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/llms-rest-functions.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/server/llms-rest-server-functions.php'; + + // Models. + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/models/class-llms-rest-api-key.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/models/class-llms-rest-webhook.php'; + + // Classes. + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-api-keys.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-api-keys-query.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-capabilities.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-install.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-webhooks.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/class-llms-rest-webhooks-query.php'; + + // Include admin classes. + if ( is_admin() ) { + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/admin/class-llms-rest-admin-settings.php'; + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/admin/class-llms-rest-admin-form-controller.php'; + } + + add_action( 'rest_api_init', array( $this, 'rest_api_includes' ), 5 ); + add_action( 'rest_api_init', array( $this, 'rest_api_controllers_init' ), 10 ); + + } + + /** + * Retrieve an instance of the API Keys management singleton. + * + * @example $keys = LLMS_REST_API()->keys(); + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_API_Keys + */ + public function keys() { + return LLMS_REST_API_Keys::instance(); + } + + /** + * Include REST api specific files. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 Include memberships controller class file. + * @since 1.0.0-beta.18 Include access plans controller class file. + * + * @return void + */ + public function rest_api_includes() { + + $includes = array( + + // Abstracts first. + 'abstracts/class-llms-rest-controller-stubs', + 'abstracts/class-llms-rest-controller', + 'abstracts/class-llms-rest-users-controller', + 'abstracts/class-llms-rest-posts-controller', + + // Functions. + 'server/llms-rest-server-functions', + + // Controllers. + 'server/class-llms-rest-api-keys-controller', + 'server/class-llms-rest-access-plans-controller', + 'server/class-llms-rest-courses-controller', + 'server/class-llms-rest-sections-controller', + 'server/class-llms-rest-lessons-controller', + 'server/class-llms-rest-memberships-controller', + 'server/class-llms-rest-enrollments-controller', + 'server/class-llms-rest-instructors-controller', + 'server/class-llms-rest-students-controller', + 'server/class-llms-rest-students-progress-controller', + 'server/class-llms-rest-webhooks-controller', + + ); + + foreach ( $includes as $include ) { + include_once LLMS_REST_API_PLUGIN_DIR . 'includes/' . $include . '.php'; + } + } + + /** + * Instantiate REST api Controllers. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 Init memberships controller. + * @since 1.0.0-beta.18 Init access plans controller. + * + * @return void + */ + public function rest_api_controllers_init() { + + $controllers = array( + 'LLMS_REST_API_Keys_Controller', + 'LLMS_REST_Courses_Controller', + 'LLMS_REST_Sections_Controller', + 'LLMS_REST_Lessons_Controller', + 'LLMS_REST_Memberships_Controller', + 'LLMS_REST_Instructors_Controller', + 'LLMS_REST_Students_Controller', + 'LLMS_REST_Students_Progress_Controller', + 'LLMS_REST_Enrollments_Controller', + 'LLMS_REST_Webhooks_Controller', + 'LLMS_REST_Access_Plans_Controller', + ); + + foreach ( $controllers as $controller ) { + $controller_instance = new $controller(); + $controller_instance->register_routes(); + } + + } + + /** + * Include all required files and classes. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.6 Load webhooks actions at init 1 instead of init 10. + * @since 1.0.0-beta.8 Load webhooks actions a little bit later: at init 6 instead of init 10, + * just after all the db tables are created (init 5), + * to avoid PHP warnings on first plugin activation. + * + * @return void + */ + public function init() { + + // only load if we have the minimum LifterLMS version installed & activated. + if ( function_exists( 'LLMS' ) && version_compare( '3.32.0', LLMS()->version, '<=' ) ) { + + // load includes. + $this->includes(); + + add_action( 'init', array( $this->webhooks(), 'load' ), 6 ); + + } + + } + + /** + * Load l10n files. + * + * This method is only used when the plugin is loaded as a standalone plugin (for development purposes), + * otherwise (when loaded as a library from within the LifterLMS core plugin) the localization + * strings are included into the LifterLMS Core plugin's po/mo files and are localized by the LifterLMS + * core plugin. + * + * Files can be found in the following order (The first loaded file takes priority): + * 1. WP_LANG_DIR/lifterlms/lifterlms-rest-LOCALE.mo + * 2. WP_LANG_DIR/plugins/lifterlms-rest-LOCALE.mo + * 3. WP_CONTENT_DIR/plugins/lifterlms-rest/i18n/lifterlms-rest-LOCALE.mo + * + * Note: The function `load_plugin_textdomain()` is not used because the same textdomain as the LifterLMS core + * is used for this plugin but the file is named `lifterlms-rest` in order to allow using a separate language + * file for each codebase. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Fixed the name of the MO loaded from the safe directory: `lifterlms-{$locale}.mo` to `lifterlms-rest-{$locale}.mo`. + * Fixed double slash typo in plugin textdomain path argument. + * Fixed issue causing language files to not load properly. + * + * @return void + */ + public function load_textdomain() { + + // load locale. + $locale = apply_filters( 'plugin_locale', get_locale(), 'lifterlms' ); + + // Load from the LifterLMS "safe" directory if it exists. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/lifterlms/lifterlms-rest-' . $locale . '.mo' ); + + // Load from the default plugins language file directory. + load_textdomain( 'lifterlms', WP_LANG_DIR . '/plugins/lifterlms-rest-' . $locale . '.mo' ); + + // Load from the plugin's language file directory. + load_textdomain( 'lifterlms', LLMS_REST_API_PLUGIN_DIR . '/i18n/lifterlms-rest-' . $locale . '.mo' ); + + } + + /** + * Retrieve an instance of the webhooks management singleton. + * + * @example $webhooks = LLMS_REST_API()->webhooks(); + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_Webhooks + */ + public function webhooks() { + return LLMS_REST_Webhooks::instance(); + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller-stubs.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller-stubs.php new file mode 100644 index 0000000000..64f9012c2c --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller-stubs.php @@ -0,0 +1,245 @@ +<?php +/** + * Base REST Controller Class. + * + * All methods which *must* be defined by extending classes are stubbed here. + * + * @package LifterLMS_REST/Abstracts + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.10 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Controller_Stubs class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Conditionally throw `_doing_it_wrong()` on stub method. + * @since 1.0.0-beta.7 Added `check_read_object_permissions()` stub. + * @since 1.0.0-beta.10 Add text domain to i18n functions. + */ +abstract class LLMS_REST_Controller_Stubs extends WP_REST_Controller { + + /** + * Base Resource + * + * For example: "courses" or "students". + * + * @var string + */ + protected $rest_base; + + /** + * Get object. + * + * @since 1.0.0-beta.1 + * + * @param int $id Object ID. + * @return object|WP_Error + */ + abstract protected function get_object( $id ); + + /** + * Determine if the current user can view the requested item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Add text domain to i18n functions. + * + * @param int $item_id WP_User id. + * @return bool + */ + protected function check_read_item_permissions( $item_id ) { + + // Translators: %s = method name. + return llms_rest_server_error( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'lifterlms' ), __METHOD__ ) ); + + } + + /** + * Determine if the current user can view the object. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.10 Add text domain to i18n functions. + * + * @param object $object Object. + * @return bool + */ + protected function check_read_object_permissions( $object ) { + + // Translators: %s = method name. + return llms_rest_server_error( sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'lifterlms' ), __METHOD__ ) ); + + } + + /** + * Insert the prepared data into the database. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from $this->get_object(). + */ + protected function create_object( $prepared, $request ) { + + // @todo: add version to message. + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::create_object', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return $this->get_object( $this->get_object_id( $prepared ) ); + + } + + /** + * Retrieve an ID from the object + * + * @since 1.0.0-beta.1 + * + * @param obj $object Item object. + * @return int + */ + protected function get_object_id( $object ) { + if ( is_object( $object ) && ! empty( $object->id ) ) { + return $object->id; + } elseif ( is_array( $object ) && ! empty( $object['id'] ) ) { + return $object['id']; + } elseif ( method_exists( $object, 'get_id' ) ) { + return $object->get_id(); + } elseif ( method_exists( $object, 'get' ) ) { + return $object->get( 'id' ); + } + + // @todo: add version to message. + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::get_object_id', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return 0; + + } + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return object + */ + protected function get_objects_query( $prepared, $request ) { + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::get_objects_query', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return new WP_Query( $prepared ); + + } + + /** + * Retrieve an array of objects from the result of $this->get_objects_query(). + * + * @since 1.0.0-beta.1 + * + * @param obj $query Objects query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::get_objects_from_query', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return array(); + + } + + /** + * Retrieve pagination information from an objects query. + * + * @since 1.0.0-beta.1 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::get_pagination_data_from_query', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return array( + 'current_page' => 1, + 'total_results' => 1, + 'total_pages' => 1, + ); + + } + + /** + * Prepare an object for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Conditionally throw `_doing_it_wrong()`. + * + * @param LLMS_Abstract_User_Data $object User object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_object_for_response( $object, $request ) { + + if ( ! method_exists( $object, 'get' ) ) { + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::prepare_object_for_response', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + } + + $prepared = array(); + $map = array_flip( $this->map_schema_to_database() ); + $fields = $this->get_fields_for_response( $request ); + + foreach ( $map as $db_key => $schema_key ) { + if ( in_array( $schema_key, $fields, true ) ) { + $prepared[ $schema_key ] = $object->get( $db_key ); + } + } + + return $prepared; + + } + + /** + * Update the object in the database with prepared data. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from $this->get_object(). + */ + protected function update_object( $prepared, $request ) { + + // @todo: add version to message. + + // Translators: %s = method name. + _doing_it_wrong( 'LLMS_REST_Controller::update_object', sprintf( __( "Method '%s' must be overridden.", 'lifterlms' ), __METHOD__ ), '1.0.0-beta.1' ); + + // For example. + return $this->get_object( $prepared['id'] ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller.php new file mode 100644 index 0000000000..5ea8d2c24f --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-controller.php @@ -0,0 +1,767 @@ +<?php +/** + * Base REST Controller + * + * @package LifterLMS_REST/Abstracts + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Controller class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Fix an issue displaying a last page for lists with 0 possible results & handle error conditions early in responses. + * @since 1.0.0-beta.7 Break `get_items()` method into `prepare_collection_query_args()`, `prepare_args_for_total_count_query()`, + * `prepare_collection_items_for_response()` and `add_header_pagination()` methods so to improve abstraction. + * `prepare_objects_query()` renamed to `prepare_collection_query_args()`. + * @since 1.0.0-beta.12 Added logic to perform a collection search. + * Added `object_inserted()` and `object_completely_inserted()` methods called after an object is + * respectively inserted in the DB and all its additional fields have been updated as well (completely inserted). + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +abstract class LLMS_REST_Controller extends LLMS_REST_Controller_Stubs { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'llms/v1'; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + ); + + /** + * Whether search is allowed + * + * @var boolean + */ + protected $is_searchable = false; + + /** + * Create an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Call `object_inserted` and `object_completely_inserted` after an object is + * respectively inserted in the DB and all its additional fields have been + * updated as well (completely inserted). + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + + if ( ! empty( $request['id'] ) ) { + return llms_rest_bad_request_error( __( 'Cannot create an existing resource.', 'lifterlms' ) ); + } + + $item = $this->prepare_item_for_database( $request ); + $object = $this->create_object( $item, $request ); + $schema = $this->get_item_schema(); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $this->object_inserted( $object, $request, $schema, true ); + + $fields_update = $this->update_additional_fields_for_object( $item, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $this->object_completely_inserted( $object, $request, $schema, true ); + + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $object, $request ); + $response = rest_ensure_response( $response ); + + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $this->get_object_id( $object ) ) ) ); + + return $response; + + } + + /** + * Called right after a resource is inserted (created/updated). + * + * @since 1.0.0-beta.12 + * + * @param object $object Inserted or updated object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + protected function object_inserted( $object, $request, $schema, $creating ) { + + $type = $this->get_object_type(); + /** + * Fires after a single llms resource is created or updated via the REST API. + * + * The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing. + * + * @since 1.0.0-beta.12 + * + * @param object $object Inserted or updated object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_insert_{$type}", $object, $request, $schema, $creating ); + } + + /** + * Called right after a resource is completely inserted (created/updated). + * + * @since 1.0.0-beta.12 + * + * @param LLMS_Post $object Inserted or updated object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + protected function object_completely_inserted( $object, $request, $schema, $creating ) { + + $type = $this->get_object_type(); + /** + * Fires after a single llms resource is completely created or updated via the REST API. + * + * The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing. + * + * @since 1.0.0-beta.12 + * + * @param object $object Inserted or updated object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_after_insert_{$type}", $object, $request, $schema, $creating ); + } + + /** + * Delete the item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + + $object = $this->get_object( $request['id'], false ); + + // We don't return 404s for items that are not found. + if ( ! is_wp_error( $object ) ) { + + // If there was an error deleting the object return the error. If the error is that the object doesn't exist return 204 below! + $del = $this->delete_object( $object, $request ); + if ( is_wp_error( $del ) ) { + return $del; + } + } + + $response = rest_ensure_response( null ); + $response->set_status( 204 ); + + return $response; + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Added `search_columns` collection param for searchable resources. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'view'; + + // We're not currently implementing searching for all of our controllers. + if ( empty( $this->is_searchable ) ) { + unset( $query_params['search'] ); + } elseif ( ! empty( $this->search_columns_mapping ) ) { + + $search_columns = array_keys( $this->search_columns_mapping ); + + $query_params['search_columns'] = array( + 'description' => __( 'Column names to be searched. Accepts a single column or a comma separated list of columns.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $search_columns, + ), + 'default' => $search_columns, + ); + } + + // page and per_page params are already specified in WP_Rest_Controller->get_collection_params(). + + $query_params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'asc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $query_params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'lifterlms' ), + 'type' => 'string', + 'default' => $this->orderby_properties[0], + 'enum' => $this->orderby_properties, + 'validate_callback' => 'rest_validate_request_arg', + ); + + $query_params['include'] = array( + 'description' => __( 'Limit results to a list of ids. Accepts a single id or a comma separated list of ids.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $query_params['exclude'] = array( + 'description' => __( 'Exclude a list of ids from results. Accepts a single id or a comma separated list of ids.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Get a single item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $response = $this->prepare_item_for_response( $object, $request ); + + return rest_ensure_response( $response ); + + } + + /** + * Retrieves all items + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Fix an issue displaying a last page for lists with 0 possible results. + * @since 1.0.0-beta.7 Broken into several methods so to improve abstraction. + * @since 1.0.0-beta.12 Return early if `prepare_collection_query_args()` is a `WP_Error`. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + + $prepared = $this->prepare_collection_query_args( $request ); + if ( is_wp_error( $prepared ) ) { + return $prepared; + } + + $query = $this->get_objects_query( $prepared, $request ); + $pagination = $this->get_pagination_data_from_query( $query, $prepared, $request ); + + // Out-of-bounds, run the query again on page one to get a proper total count. + if ( $pagination['total_results'] < 1 ) { + + $prepared_for_total_count = $this->prepare_args_for_total_count_query( $prepared, $request ); + $count_query = $this->get_objects_query( $prepared_for_total_count, $request ); + $count_results = $this->get_pagination_data_from_query( $count_query, $prepared_for_total_count, $request ); + + $pagination['total_results'] = $count_results['total_results']; + } + + if ( $pagination['current_page'] > $pagination['total_pages'] && $pagination['total_results'] > 0 ) { + return llms_rest_bad_request_error( __( 'The page number requested is larger than the number of pages available.', 'lifterlms' ) ); + } + + $objects = $this->get_objects_from_query( $query ); + $items = $this->prepare_collection_items_for_response( $objects, $request ); + + $response = rest_ensure_response( $items ); + $response = $this->add_header_pagination( $response, $pagination, $request ); + + return $response; + + } + + /** + * Format query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.12 Prepare args for search and call collection params to query args map method. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + // Prepare all set args. + $registered = $this->get_collection_params(); + $prepared = array(); + + foreach ( $registered as $key => $value ) { + if ( isset( $request[ $key ] ) ) { + $prepared[ $key ] = $request[ $key ]; + } + } + + $prepared = $this->prepare_collection_query_search_args( $prepared, $request ); + if ( is_wp_error( $prepared ) ) { + return $prepared; + } + + $prepared = $this->map_params_to_query_args( $prepared, $registered, $request ); + + return $prepared; + + } + + /** + * Map schema to query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.12 + * + * @param array $prepared Array of collection arguments. + * @param array $registered Registered collection params. + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function map_params_to_query_args( $prepared, $registered, $request ) { + return $prepared; + } + + /** + * Format search query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.12 + * @since 1.0.0-beta.21 Return an error if requesting a list ordered by 'relevance' without providing a search string. + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array|WP_Error + */ + protected function prepare_collection_query_search_args( $prepared, $request ) { + + // Search? + if ( ! empty( $prepared['search'] ) ) { + + if ( ! empty( $this->search_columns_mapping ) ) { + + if ( empty( $prepared['search_columns'] ) ) { + return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) ); + } + + // Filter search columns by context. + $search_columns = array_keys( $this->filter_response_by_context( array_flip( $prepared['search_columns'] ), $request['context'] ) ); + + // Check if one of more unallowed search columns have been provided as request query params (not merged with defaults). + if ( ! empty( $request->get_query_params()['search_columns'] ) ) { + + $forbidden_columns = array_diff( $prepared['search_columns'], $search_columns ); + + if ( ! empty( $forbidden_columns ) ) { + return llms_rest_authorization_required_error( + sprintf( + // Translators: %1$s comma separated list of search columns. + __( 'You are not allowed to search into the provided column(s): %1$s', 'lifterlms' ), + implode( ',', $forbidden_columns ) + ) + ); + } + } + + $prepared['search_columns'] = array(); + + // Map our search columns into query compatible ones. + foreach ( $search_columns as $search_column ) { + if ( isset( $this->search_columns_mapping[ $search_column ] ) ) { + $prepared['search_columns'][] = $this->search_columns_mapping[ $search_column ]; + } + } + + if ( empty( $prepared['search_columns'] ) ) { + return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) ); + } + } + + $prepared['search'] = '*' . $prepared['search'] . '*'; + + } else { + + // Ensure a search string is set in case the orderby is set to 'relevance'. + if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] ) { + return llms_rest_bad_request_error( + __( 'You need to define a search term to order by relevance.', 'lifterlms' ) + ); + } + } + + return $prepared; + } + + /** + * Prepare query args for total count query. + * + * @since 1.0.0-beta.7 + * + * @param array $args Array of query args. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_args_for_total_count_query( $args, $request ) { + // Run the query again without pagination to get a proper total count. + unset( $args['paged'], $args['page'] ); + return $args; + } + + /** + * Prepare collection items for response. + * + * @since 1.0.0-beta.7 + * + * @param array $objects Array of objects to be prepared for response. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + + foreach ( $objects as $object ) { + $object = $this->get_object( $object, false ); + + if ( ! $this->check_read_object_permissions( $object ) ) { + continue; + } + + $item = $this->prepare_item_for_response( $object, $request ); + if ( ! is_wp_error( $item ) ) { + $items[] = $this->prepare_response_for_collection( $item ); + } + } + + return $items; + } + + /** + * Add pagination info and links to the response header. + * + * @since 1.0.0-beta.7 + * + * @param WP_REST_Response $response Current response being served. + * @param array $pagination Pagination array. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response + */ + protected function add_header_pagination( $response, $pagination, $request ) { + + $response->header( 'X-WP-Total', $pagination['total_results'] ); + $response->header( 'X-WP-TotalPages', $pagination['total_pages'] ); + + $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $request->get_route() ) ); + + // First page link. + if ( 1 !== $pagination['current_page'] ) { + $first_link = add_query_arg( 'page', 1, $base ); + $response->link_header( 'first', $first_link ); + } + + // Previous page link. + if ( $pagination['current_page'] > 1 ) { + $prev_page = $pagination['current_page'] - 1; + if ( $prev_page > $pagination['total_pages'] ) { + $prev_page = $pagination['total_pages']; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + + // Next page link. + if ( $pagination['total_pages'] > $pagination['current_page'] ) { + $next_link = add_query_arg( 'page', $pagination['current_page'] + 1, $base ); + $response->link_header( 'next', $next_link ); + } + + // Last page link. + if ( $pagination['total_pages'] && $pagination['total_pages'] !== $pagination['current_page'] ) { + $last_link = add_query_arg( 'page', $pagination['total_pages'], $base ); + $response->link_header( 'last', $last_link ); + } + + return $response; + + } + + /** + * Retrieves the query params for retrieving a single resource. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_get_item_params() { + + return array( + 'context' => $this->get_context_param( + array( + 'default' => 'view', + ) + ), + ); + + } + + /** + * Retrieve arguments for deleting a resource. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_delete_item_args() { + return array(); + } + + /** + * Map request keys to database keys for insertion. + * + * Array keys are the request fields (as defined in the schema) and + * array values are the database fields. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + protected function map_schema_to_database() { + + $schema = $this->get_item_schema(); + $keys = array_keys( $schema['properties'] ); + return array_combine( $keys, $keys ); + + } + + /** + * Prepare request arguments for a database insert/update. + * + * @since 1.0.0-beta.1 + * + * @param WP_Rest_Request $request Request object. + * @return array + */ + protected function prepare_item_for_database( $request ) { + + $prepared = array(); + $map = $this->map_schema_to_database(); + $schema = $this->get_item_schema(); + + foreach ( $map as $req_key => $db_key ) { + if ( ! empty( $request[ $req_key ] ) ) { + $prepared[ $db_key ] = $request[ $req_key ]; + } + } + + return $prepared; + + } + + /** + * Prepares a single object for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Return early with a WP_Error if `$object` is a WP_Error. + * @since 1.0.0-beta.14 Pass the `$request` parameter to `prepare_links()`. + * + * @param obj $object Raw object from database. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $object, $request ) { + + if ( is_wp_error( $object ) ) { + return $object; + } + + $data = $this->prepare_object_for_response( $object, $request ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + // Add links. + $response->add_links( $this->prepare_links( $object, $request ) ); + + return $response; + + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added $request parameter. + * + * @param obj $object Item object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $object, $request ) { + + $base = rest_url( sprintf( '/%1$s/%2$s', $this->namespace, $this->rest_base ) ); + + $links = array( + 'self' => array( + 'href' => sprintf( '%1$s/%2$d', $base, $this->get_object_id( $object ) ), + ), + 'collection' => array( + 'href' => $base, + ), + ); + + return $links; + + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), // see class-wp-rest-controller.php. + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => $this->get_delete_item_args(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + } + + /** + * Update item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Call `object_inserted` and `object_completely_inserted` after an object is + * respectively inserted in the DB and all its additional fields have been + * updated as well (completely inserted). + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object or WP_Error on failure. + */ + public function update_item( $request ) { + + $object = $this->get_object( $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $item = $this->prepare_item_for_database( $request ); + $object = $this->update_object( $item, $request ); + $schema = $this->get_item_schema(); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $this->object_inserted( $object, $request, $schema, false ); + + $fields_update = $this->update_additional_fields_for_object( $item, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $this->object_completely_inserted( $object, $request, $schema, false ); + + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $object, $request ); + $response = rest_ensure_response( $response ); + + return $response; + + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-database-resource.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-database-resource.php new file mode 100644 index 0000000000..f16dec60f4 --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-database-resource.php @@ -0,0 +1,273 @@ +<?php +/** + * Shared functiosn for database resource management. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Database_Resource class.. + * + * @since 1.0.0-beta.1 + */ +abstract class LLMS_REST_Database_Resource { + + /** + * Resource Name/ID key. + * + * EG: key. + * + * @var string + */ + protected $id = ''; + + /** + * Resource Model classname. + * + * EG: LLMS_REST_API_Key. + * + * @var string + */ + protected $model = ''; + + /** + * Default column values (for creating). + * + * @var array + */ + protected $default_column_values = array(); + + /** + * Array of read only column names. + * + * @var array + */ + protected $read_only_columns = array( 'id' ); + + /** + * Array of required columns (for creating). + * + * @var array + */ + protected $required_columns = array(); + + /** + * Validate data supplied for creating/updating a resource. + * + * @since 1.0.0-beta.1 + * + * @param array $data Associative array of data to set to a key's properties. + * @return WP_Error|true When data is invalid will return a WP_Error with information about the invalid properties, + * otherwise `true` denoting data is valid. + */ + protected function is_data_valid( $data ) { + + return true; + + } + + /** + * Create a new Resource + * + * @since 1.0.0-beta.1 + * + * @param array $data Associative array of data to set to the resource's properties. + * @return WP_Error|obj + */ + public function create( $data ) { + + $data = $this->create_prepare( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + return $this->save( new $this->model(), $data ); + + } + + /** + * Prepare data for creation. + * + * @since 1.0.0-beta.1 + * + * @param array $data Array of data. + * @return array + */ + public function create_prepare( $data ) { + + if ( ! empty( $data['id'] ) ) { + // Translators: %s = name of the resource type (for example: "API Key"). + return new WP_Error( 'llms_rest_' . $this->id . '_exists', sprintf( __( 'Cannot create a new %s with a pre-defined ID.', 'lifterlms' ), $this->get_i18n_name() ) ); + } + + // Merge in default values. + $data = wp_parse_args( array_filter( $data ), $this->get_default_column_values() ); + + // Required Fields. + foreach ( $this->required_columns as $key ) { + + if ( empty( $data[ $key ] ) ) { + return new WP_Error( + 'llms_rest_' . $this->id . '_missing_' . $key, + // Translators: %1$s = name of the resource type; %2$s = field name. + sprintf( __( '%1$s "%2$s" is required.', 'lifterlms' ), $this->get_i18n_name(), $key ) + ); + } + } + + $err = $this->is_data_valid( $data ); + if ( is_wp_error( $err ) ) { + return $err; + } + + return $data; + + } + + /** + * Delete a the resource. + * + * @since 1.0.0-beta.1 + * + * @param int $id Resource ID. + * @return bool `true` on success, `false` if the resource couldn't be found or an error was encountered during deletion. + */ + public function delete( $id ) { + $obj = $this->get( $id, false ); + if ( $obj ) { + return $obj->delete(); + } + return false; + } + + /** + * Retrieve an API Key object instance. + * + * @since 1.0.0-beta.1 + * + * @param int $id API Key ID. + * @param bool $hydrate If true, pulls all key data from the database on instantiation. + * @return obj|false + */ + public function get( $id, $hydrate = true ) { + $obj = new $this->model( $id, $hydrate ); + if ( $obj && $obj->exists() ) { + return $obj; + } + return false; + } + + /** + * Get default column values. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_default_column_values() { + + /** + * Allow customization of default Resource values. + * + * @since 1.0.0-beta.1 + * + * @param array $values An associative array of default values. + */ + return apply_filters( 'llms_rest_' . $this->id . '_default_properties', $this->default_column_values ); + + } + + /** + * Retrieve the translated resource name. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + protected function get_i18n_name() { + return __( 'Resource', 'lifterlms' ); + } + + /** + * Update a resource. + * + * @since 1.0.0-beta.1 + * + * @param array $data { + * Array of data to update. + * + * @type int $id (Required). Resource ID. + * } + * @return [type] + */ + public function update( $data ) { + + if ( empty( $data['id'] ) ) { + // Translators: %s = name of the resource type (for example: "API Key"). + return new WP_Error( 'llms_rest_' . $this->id . '_missing_id', sprintf( __( 'No %s ID was supplied.', 'lifterlms' ), $this->get_i18n_name() ) ); + } + + $obj = $this->get( $data['id'] ); + if ( ! $obj || ! $obj->exists() ) { + // Translators: %s = name of the resource type (for example: "API Key"). + return new WP_Error( 'llms_rest_' . $this->id . '_invalid_' . $this->id, sprintf( __( 'The requested %s could not be located.', 'lifterlms' ), $this->get_i18n_name() ) ); + } + + $data = $this->update_prepare( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + return $this->save( $obj, $data ); + + } + + /** + * Prepare data for an update. + * + * @since 1.0.0-beta.1 + * + * @param array $data Associative array of data to set to a resources properties. + * @return object|WP_Error + */ + protected function update_prepare( $data ) { + + // Filter out write-protected keys. + $data = array_diff_key( + $data, + array_fill_keys( $this->read_only_columns, false ) + ); + + $err = $this->is_data_valid( $data ); + if ( is_wp_error( $err ) ) { + return $err; + } + + return $data; + + } + + /** + * Persist data. + * + * This method assumes the supplied data has already been validated and sanitized. + * + * @since 1.0.0-beta.1 + * + * @param obj $obj Instantiated object. + * @param array $data Associative array of data to persist. + * @return obj + */ + protected function save( $obj, $data ) { + + $obj->setup( $data )->save(); + return $obj; + + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-posts-controller.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-posts-controller.php new file mode 100644 index 0000000000..32f414275f --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-posts-controller.php @@ -0,0 +1,1825 @@ +<?php +/** + * REST LLMS Posts Controller Class + * + * @package LifterLMS_REST/Abstracts + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.21 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Posts_Controller + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`. + * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. + * @since 1.0.0-beta.7 Added: `check_read_object_permissions()`, `get_objects_from_query()`, `get_objects_query()`, `get_pagination_data_from_query()`, `prepare_collection_items_for_response()` methods overrides. + * `get_items()` method removed, now abstracted in LLMS_REST_Controller. + * `prepare_objects_query()` renamed to `prepare_collection_query_args()`. + * On `update_item`, don't execute `$object->set_bulk()` when there's no data to update. + * Fix wp:featured_media link, we don't expose any embeddable field. + * Also `self` and `collection` links prepared in the parent class. + * Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks: + * fired after inserting/updateing an llms post into the database. + * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route. + * Initialize `$prepared_item` array before adding values to it. + * @since 1.0.0-beta.9 Implemented a generic way to create and get an llms post object instance given a `post_type`. + * In `get_objects_from_query()` avoid performing an additional query, just return the already retrieved posts. + * Removed `"llms_rest_{$this->post_type}_filters_removed_for_reponse"` filter hooks, + * `"llms_rest_{$this->post_type}_filters_removed_for_response"` added. + * @since 1.0.0-beta.11 Fixed `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks fourth param: + * must be false when updating. + * @since 1.0.0-beta.12 Moved parameters to query args mapping from `$this->prepare_collection_params()` to `$this->map_params_to_query_args()`. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + * @since 1.0.0-beta.21 Enable search. + */ +abstract class LLMS_REST_Posts_Controller extends LLMS_REST_Controller { + + /** + * Post type. + * + * @var string + */ + protected $post_type; + + /** + * Route base. + * + * @var string + */ + protected $collection_route_base_for_pagination; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'title', + 'date_created', + 'date_updated', + 'menu_order', + 'relevance', + ); + + /** + * Whether search is allowed + * + * @var boolean + */ + protected $is_searchable = true; + + /** + * LLMS post class name. + * + * @since 1.0.0-beta.9 + * @var string; + */ + protected $llms_post_class; + + /** + * Retrieves an array of arguments for the delete endpoint. + * + * @since 1.0.0-beta.1 + * + * @return array Delete endpoint arguments. + */ + public function get_delete_item_args() { + + return array( + 'force' => array( + 'description' => __( 'Bypass the trash and force course deletion.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + ), + ); + + } + + /** + * Retrieves the query params for retrieving a single resource. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_get_item_params() { + + $params = parent::get_get_item_params(); + $schema = $this->get_item_schema(); + + if ( isset( $schema['properties']['password'] ) ) { + $params['password'] = array( + 'description' => __( 'Post password. Required if the post is password protected.', 'lifterlms' ), + 'type' => 'string', + ); + } + + return $params; + + } + + /** + * Determine if the current user can view the object. + * + * @since 1.0.0-beta.7 + * + * @param object $object Object. + * @return bool + */ + protected function check_read_object_permissions( $object ) { + return $this->check_read_permission( $object ); + } + + /** + * Check if a given request has access to read items. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + + // Everybody can list llms posts (in read mode). + if ( 'edit' === $request['context'] && ! $this->check_update_permission() ) { + return llms_rest_authorization_required_error(); + } + + return true; + + } + + /** + * Retrieve pagination information from an objects query. + * + * @since 1.0.0-beta.7 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + $total_results = (int) $query->found_posts; + $current_page = isset( $prepared['paged'] ) ? (int) $prepared['paged'] : 1; + $total_pages = (int) ceil( $total_results / (int) $query->get( 'posts_per_page' ) ); + + return compact( 'current_page', 'total_results', 'total_pages' ); + + } + + /** + * Check if a given request has access to create an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use plural post type name. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + + $post_type_object = get_post_type_object( $this->post_type ); + $post_type_name = $post_type_object->labels->name; + + if ( ! empty( $request['id'] ) ) { + // Translators: %s = The post type name. + return llms_rest_bad_request_error( sprintf( __( 'Cannot create existing %s.', 'lifterlms' ), $post_type_name ) ); + } + + if ( ! $this->check_create_permission() ) { + // Translators: %s = The post type name. + return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to create %s as this user.', 'lifterlms' ), $post_type_name ) ); + } + + if ( ! $this->check_assign_terms_permission( $request ) ) { + return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to assign the provided terms.', 'lifterlms' ) ); + } + + return true; + } + + + /** + * Creates a single LLMS post. + * + * Extending classes can add additional object fields by overriding the method `update_additional_object_fields()`. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks: + * fired after inserting/uodateing an llms post into the database. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + + $prepared_item = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_item ) ) { + return $prepared_item; + } + + $object = $this->create_llms_post( $prepared_item ); + if ( is_wp_error( $object ) ) { + + if ( 'db_insert_error' === $object->get_error_code() ) { + $object->add_data( array( 'status' => 500 ) ); + } else { + $object->add_data( array( 'status' => 400 ) ); + } + + return $object; + } + + $schema = $this->get_item_schema(); + + /** + * Fires after a single llms post is created or updated via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @since 1.0.0-beta.7 + * + * @param LLMS_Post $object Inserted or updated llms object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_insert_{$this->post_type}", $object, $request, $schema, true ); + + // Set all the other properties. + // TODO: maybe we want to filter the post properties that have already been inserted before. + $set_bulk_result = $object->set_bulk( $prepared_item, true ); + if ( is_wp_error( $set_bulk_result ) ) { + + if ( 'db_update_error' === $set_bulk_result->get_error_code() ) { + $set_bulk_result->add_data( array( 'status' => 500 ) ); + } else { + $set_bulk_result->add_data( array( 'status' => 400 ) ); + } + + return $set_bulk_result; + } + + $object_id = $object->get( 'id' ); + + $additional_fields = $this->update_additional_object_fields( $object, $request, $schema, $prepared_item ); + if ( is_wp_error( $additional_fields ) ) { + return $additional_fields; + } + + if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + $this->handle_featured_media( $request['featured_media'], $object_id ); + } + + $terms_update = $this->handle_terms( $object_id, $request ); + if ( is_wp_error( $terms_update ) ) { + return $terms_update; + } + + /** + * TODO: understand how to treat possible conflicting properties => instructors are registered as additional rest field by llms_blocks + */ + // $fields_update = $this->update_additional_fields_for_object( $object, $request ); + // if ( is_wp_error( $fields_update ) ) { + // return $fields_update; + // } + $request->set_param( 'context', 'edit' ); + + /** + * Fires after a single llms post is completely created or updated via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @since 1.0.0-beta.7 + * + * @param LLMS_Post $object Inserted or updated llms object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_after_insert_{$this->post_type}", $object, $request, $schema, true ); + + $response = $this->prepare_item_for_response( $object, $request ); + + $response->set_status( 201 ); + + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $object_id ) ) ); + + return $response; + } + + /** + * Check if a given request has access to read an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + if ( 'edit' === $request['context'] && ! $this->check_update_permission( $object ) ) { + return llms_rest_authorization_required_error(); + } + + if ( ! empty( $request['password'] ) ) { + // Check post password, and return error if invalid. + if ( ! hash_equals( $object->get( 'password' ), $request['password'] ) ) { + return llms_rest_authorization_required_error( __( 'Incorrect password.', 'lifterlms' ) ); + } + } + + // Allow access to all password protected posts if the context is edit. + if ( 'edit' === $request['context'] ) { + add_filter( 'post_password_required', '__return_false' ); + } + + if ( ! $this->check_read_permission( $object ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + } + + /** + * Retrieves the query params for the objects collection + * + * @since 1.0.0-beta.19 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $query_params = parent::get_collection_params(); + $schema = $this->get_item_schema(); + + if ( isset( $schema['properties']['status'] ) ) { + $query_params['status'] = array( + 'default' => 'publish', + 'description' => __( 'Limit result set to posts assigned one or more statuses.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'enum' => array_merge( + array_keys( + get_post_stati() + ), + array( + 'any', + ) + ), + 'type' => 'string', + ), + 'sanitize_callback' => array( $this, 'sanitize_post_statuses' ), + ); + } + + return $query_params; + + } + + /** + * Format query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.12 Moved parameters to query args mapping into a different method. + * @since 1.0.0-beta.18 Correctly return errors. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $prepared = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $prepared ) ) { + return $prepared; + } + + // Force the post_type argument, since it's not a user input variable. + $prepared['post_type'] = $this->post_type; + + $query_args = $this->prepare_items_query( $prepared, $request ); + + return $query_args; + + } + + /** + * Map schema to query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.12 + * @since 1.0.0-beta.19 Map 'status' collection param to to 'post_status' query arg. + * + * @param array $prepared Array of collection arguments. + * @param array $registered Registered collection params. + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function map_params_to_query_args( $prepared, $registered, $request ) { + + $args = array(); + + /* + * This array defines mappings between public API query parameters whose + * values are accepted as-passed, and their internal WP_Query parameter + * name equivalents (some are the same). Only values which are also + * present in $registered will be set. + */ + $parameter_mappings = array( + 'order' => 'order', + 'orderby' => 'orderby', + 'page' => 'paged', + 'exclude' => 'post__not_in', + 'include' => 'post__in', + 'search' => 's', + 'status' => 'post_status', + ); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $args. + */ + foreach ( $parameter_mappings as $api_param => $wp_param ) { + if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { + $args[ $wp_param ] = $request[ $api_param ]; + } + } + + // Ensure our per_page parameter overrides any provided posts_per_page filter. + if ( isset( $registered['per_page'] ) ) { + $args['posts_per_page'] = $request['per_page']; + } + + return $args; + } + + /** + * Check if a given request has access to update an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use plural post type name. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $post_type_object = get_post_type_object( $this->post_type ); + $post_type_name = $post_type_object->labels->name; + + if ( ! $this->check_update_permission( $object ) ) { + // Translators: %s = The post type name. + return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to update %s as this user.', 'lifterlms' ), $post_type_name ) ); + } + + if ( ! $this->check_assign_terms_permission( $request ) ) { + return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to assign the provided terms.', 'lifterlms' ) ); + } + + return true; + } + + /** + * Updates a single llms post. + * + * Extending classes can add additional object fields by overriding the method `update_additional_object_fields()`. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Don't execute `$object->set_bulk()` when there's no data to update: + * this fixes an issue when updating only properties which are not handled in `prepare_item_for_database()`. + * Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks: + * fired after inserting/uodateing an llms post into the database. + * @since 1.0.0-beta.11 Fixed `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks fourth param: + * must be false when updating. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function update_item( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $prepared_item = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_item ) ) { + return $prepared_item; + } + + $update_result = empty( array_diff_key( $prepared_item, array_flip( array( 'id' ) ) ) ) ? false : $object->set_bulk( $prepared_item, true ); + if ( is_wp_error( $update_result ) ) { + + if ( 'db_update_error' === $update_result->get_error_code() ) { + $update_result->add_data( array( 'status' => 500 ) ); + } else { + $update_result->add_data( array( 'status' => 400 ) ); + } + + return $update_result; + } + + $schema = $this->get_item_schema(); + + /** + * Fires after a single llms post is created or updated via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @since 1.0.0-beta.7 + * + * @param LLMS_Post $object Inserted or updated llms object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_insert_{$this->post_type}", $object, $request, $schema, false ); + + $object_id = $object->get( 'id' ); + + $additional_fields = $this->update_additional_object_fields( $object, $request, $schema, $prepared_item, false ); + if ( is_wp_error( $additional_fields ) ) { + return $additional_fields; + } + + if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + $this->handle_featured_media( $request['featured_media'], $object_id ); + } + + $terms_update = $this->handle_terms( $object_id, $request ); + if ( is_wp_error( $terms_update ) ) { + return $terms_update; + } + + /** + * TODO: understand how to treat possible conflicting properties => instructors are registered as additional rest field by llms_blocks + */ + // $fields_update = $this->update_additional_fields_for_object( $object, $request ); + // if ( is_wp_error( $fields_update ) ) { + // return $fields_update; + // } + $request->set_param( 'context', 'edit' ); + + /** + * Fires after a single llms post is completely created or updated via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @since 1.0.0-beta.7 + * + * @param LLMS_Post $object Inserted or updated llms object. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + do_action( "llms_rest_after_insert_{$this->post_type}", $object, $request, $schema, false ); + + return $this->prepare_item_for_response( $object, $request ); + + } + + /** + * Updates a single llms post. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 return description updated. + * + * @param LLMS_Post_Model $object LMMS_Post_Model instance. + * @param array $prepared_item Array. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update. + */ + protected function update_additional_object_fields( $object, $prepared_item, $request, $schema ) { + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Provide a more significant error message when trying to delete an item without permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + // LLMS_Post not found, we don't return a 404. + if ( in_array( 'llms_rest_not_found', $object->get_error_codes(), true ) ) { + return true; + } + + return $object; + } + + if ( ! $this->check_delete_permission( $object ) ) { + return llms_rest_authorization_required_error( + sprintf( + // Translators: %s = The post type name. + __( 'Sorry, you are not allowed to delete %s as this user.', 'lifterlms' ), + get_post_type_object( $this->post_type )->labels->name + ) + ); + } + + return true; + + } + + /** + * Deletes a single llms post. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + $response = new WP_REST_Response(); + $response->set_status( 204 ); + + if ( is_wp_error( $object ) ) { + // Course not found, we don't return a 404. + if ( in_array( 'llms_rest_not_found', $object->get_error_codes(), true ) ) { + return $response; + } + + return $object; + } + + $post_type_object = get_post_type_object( $this->post_type ); + $post_type_name = $post_type_object->labels->singular_name; + + $id = $object->get( 'id' ); + $force = $this->is_delete_forced( $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $result = wp_delete_post( $id, true ); + } else { + + $supports_trash = $this->is_trash_supported(); + + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( + 'llms_rest_trash_not_supported', + /* translators: %1$s: post type name, %2$s: force=true */ + sprintf( __( 'The %1$s does not support trashing. Set \'%2$s\' to delete.', 'lifterlms' ), $post_type_name, 'force=true' ), + array( 'status' => 501 ) + ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' !== $object->get( 'status' ) ) { + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $result = wp_trash_post( $id ); + } else { + $result = true; + } + + $request->set_param( 'context', 'edit' ); + $object = $this->get_object( $id ); + $response = $this->prepare_item_for_response( $object, $request ); + + } + + if ( ! $result ) { + return new WP_Error( + 'llms_rest_cannot_delete', + /* translators: %s: post type name */ + sprintf( __( 'The %s cannot be deleted.', 'lifterlms' ), $post_type_name ), + array( 'status' => 500 ) + ); + } + + return $response; + + } + + /** + * Whether the delete should be forced. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return bool True if the delete should be forced, false otherwise. + */ + protected function is_delete_forced( $request ) { + return isset( $request['force'] ) && (bool) $request['force']; + } + + /** + * Whether the trash is supported. + * + * @since 1.0.0-beta.1 + * + * @return bool True if the trash is supported, false otherwise. + */ + protected function is_trash_supported() { + return ( EMPTY_TRASH_DAYS > 0 ); + } + + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request. + * + * @since 1.0.0-beta.7 + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Full details about the request. + * @return WP_Query + */ + protected function get_objects_query( $prepared, $request ) { + + return new WP_Query( $prepared ); + + } + + /** + * Retrieve an array of objects from the result of `$this->get_objects_query()`. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.9 Avoid performing an additional query, just return the already retrieved posts. + * + * @param WP_Query $query WP_Query query result. + * @return WP_Post[] + */ + protected function get_objects_from_query( $query ) { + + return $query->posts; + + } + + /** + * Prepare collection items for response. + * + * @since 1.0.0-beta.7 + * + * @param array $objects Array of objects to be prepared for response. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + + // Allow access to all password protected posts if the context is edit. + if ( 'edit' === $request['context'] ) { + add_filter( 'post_password_required', '__return_false' ); + } + + $items = parent::prepare_collection_items_for_response( $objects, $request ); + + // Reset filter. + if ( 'edit' === $request['context'] ) { + remove_filter( 'post_password_required', '__return_false' ); + } + + return $items; + + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object object object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $object, $request ) { + + $object_id = $object->get( 'id' ); + $password_required = post_password_required( $object_id ); + $password = $object->get( 'password' ); + + $data = array( + 'id' => $object->get( 'id' ), + 'date_created' => $object->get_date( 'date', 'Y-m-d H:i:s' ), + 'date_created_gmt' => $object->get_date( 'date_gmt', 'Y-m-d H:i:s' ), + 'date_updated' => $object->get_date( 'modified', 'Y-m-d H:i:s' ), + 'date_updated_gmt' => $object->get_date( 'modified_gmt', 'Y-m-d H:i:s' ), + 'menu_order' => $object->get( 'menu_order' ), + 'title' => array( + 'raw' => $object->get( 'title', true ), + 'rendered' => $object->get( 'title' ), + ), + 'password' => $password, + 'slug' => $object->get( 'name' ), + 'post_type' => $this->post_type, + 'permalink' => get_permalink( $object_id ), + 'status' => $object->get( 'status' ), + 'featured_media' => (int) get_post_thumbnail_id( $object_id ), + 'comment_status' => $object->get( 'comment_status' ), + 'ping_status' => $object->get( 'ping_status' ), + 'content' => array( + 'raw' => $object->get( 'content', true ), + 'rendered' => $password_required ? '' : apply_filters( 'the_content', $object->get( 'content', true ) ), + 'protected' => (bool) $password, + ), + 'excerpt' => array( + 'raw' => $object->get( 'excerpt', true ), + 'rendered' => $password_required ? '' : apply_filters( 'the_excerpt', $object->get( 'excerpt' ) ), + 'protected' => (bool) $password, + ), + ); + + return $data; + + } + + /** + * Prepare a single item for the REST response + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Pass the `$request` parameter to `prepare_links()`. + * + * @param LLMS_Post_Model $object LLMS post object. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $object, $request ) { + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + + // Need to set the global $post because of references to the global $post when e.g. filtering the content, or processing blocks/shortcodes. + global $post; + $temp = $post; + $post = $object->get( 'post' ); // phpcs:ignore + setup_postdata( $post ); + + $removed_filters_for_response = $this->maybe_remove_filters_for_response( $object ); + + $has_password_filter = false; + + if ( $this->can_access_password_content( $object, $request ) ) { + // Allow access to the post, permissions already checked before. + add_filter( 'post_password_required', '__return_false' ); + $has_password_filter = true; + } + + $data = $this->prepare_object_for_response( $object, $request ); + + if ( $has_password_filter ) { + // Reset filter. + remove_filter( 'post_password_required', '__return_false' ); + } + + $this->maybe_add_removed_filters_for_response( $removed_filters_for_response ); + $post = $temp; // phpcs:ignore + wp_reset_postdata(); + + // Filter data including only schema props. + $data = array_intersect_key( $data, array_flip( $this->get_fields_for_response( $request ) ) ); + + // Filter data by context. E.g. in "view" mode the password property won't be allowed. + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $object, $request ) ); + + return $response; + } + + /** + * Determines the allowed query_vars for a get_items() response and prepares + * them for WP_Query. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param WP_REST_Request $request Optional. Full details about the request. + * @return array Items query arguments. + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $query_args = array(); + + foreach ( $prepared_args as $key => $value ) { + $query_args[ $key ] = $value; + } + + $query_args = $this->prepare_items_query_orderby_mappings( $query_args, $request ); + + // Turn exclude and include params into proper arrays. + foreach ( array( 'post__in', 'post__not_in' ) as $arg ) { + if ( isset( $query_args[ $arg ] ) && ! is_array( $query_args[ $arg ] ) ) { + $query_args[ $arg ] = array_map( 'absint', explode( ',', $query_args[ $arg ] ) ); + } + } + + return $query_args; + + } + + /** + * Map to proper WP_Query orderby param. + * + * @since 1.0.0-beta.1 + * + * @param array $query_args WP_Query arguments. + * @param WP_REST_Request $request Full details about the request. + * @return array Query arguments. + */ + protected function prepare_items_query_orderby_mappings( $query_args, $request ) { + + // Map to proper WP_Query orderby param. + if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) { + $orderby_mappings = array( + 'id' => 'ID', + 'title' => 'title', + 'data_created' => 'post_date', + 'date_updated' => 'post_modified', + ); + + if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) { + $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ]; + } + } + + return $query_args; + + } + + /** + * Prepares a single post for create or update. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.8 Initialize `$prepared_item` array before adding values to it. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of llms post args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + + $prepared_item = array(); + + // LLMS Post ID. + if ( isset( $request['id'] ) ) { + $existing_object = $this->get_object( absint( $request['id'] ) ); + if ( is_wp_error( $existing_object ) ) { + return $existing_object; + } + + $prepared_item['id'] = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // LLMS Post title. + if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { + if ( is_string( $request['title'] ) ) { + $prepared_item['post_title'] = $request['title']; + } elseif ( ! empty( $request['title']['raw'] ) ) { + $prepared_item['post_title'] = $request['title']['raw']; + } + } + + // LLMS Post content. + if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) { + if ( is_string( $request['content'] ) ) { + $prepared_item['post_content'] = $request['content']; + } elseif ( isset( $request['content']['raw'] ) ) { + $prepared_item['post_content'] = $request['content']['raw']; + } + } + + // LLMS Post excerpt. + if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) { + if ( is_string( $request['excerpt'] ) ) { + $prepared_item['post_excerpt'] = $request['excerpt']; + } elseif ( isset( $request['excerpt']['raw'] ) ) { + $prepared_item['post_excerpt'] = $request['excerpt']['raw']; + } + } + + // LLMS Post status. + if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) { + $status = $this->handle_status_param( $request['status'] ); + if ( is_wp_error( $status ) ) { + return $status; + } + + $prepared_item['post_status'] = $status; + } + + // LLMS Post date. + if ( ! empty( $schema['properties']['date_created'] ) && ! empty( $request['date_created'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created'] ); + + if ( ! empty( $date_data ) ) { + list( $prepared_item['post_date'], $prepared_item['post_date_gmt'] ) = $date_data; + $prepared_item['edit_date'] = true; + } + } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); + + if ( ! empty( $date_data ) ) { + list( $prepared_item['post_date'], $prepared_item['post_date_gmt'] ) = $date_data; + $prepared_item['edit_date'] = true; + } + } + + // LLMS Post slug. + if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) { + $prepared_item['post_name'] = $request['slug']; + } + + // LLMS Post password. + if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) { + $prepared_item['post_password'] = $request['password']; + } + + // LLMS Post Menu order. + if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) { + $prepared_item['menu_order'] = (int) $request['menu_order']; + } + + // LLMS Post Comment status. + if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) { + $prepared_item['comment_status'] = $request['comment_status']; + } + + // LLMS Post Ping status. + if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) { + $prepared_item['ping_status'] = $request['ping_status']; + } + + return $prepared_item; + + } + + /** + * Get the LLMS Posts's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.19 Allow only _built_in and not internal post status (see WordPress `get_post_stati()` ). + * + * @return array + */ + public function get_item_schema() { + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique Identifier. The WordPress Post ID.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'Creation date. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'Creation date (in GMT). Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_updated' => array( + 'description' => __( 'Date last modified. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_updated_gmt' => array( + 'description' => __( 'Date last modified (in GMT). Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Creation date (in GMT). Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'title' => array( + 'description' => __( 'Post title.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'required' => true, + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw title. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered title.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'The HTML content of the post.', 'lifterlms' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'required' => true, + 'properties' => array( + 'rendered' => array( + 'description' => __( 'Rendered HTML content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'raw' => array( + 'description' => __( 'Raw HTML content. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'protected' => array( + 'description' => __( 'Whether the content is protected with a password.', 'lifterlms' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'excerpt' => array( + 'type' => 'object', + 'description' => __( 'The HTML excerpt of the post.', 'lifterlms' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'rendered' => array( + 'description' => __( 'Rendered HTML excerpt.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'raw' => array( + 'description' => __( 'Raw HTML excerpt. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'protected' => array( + 'description' => __( 'Whether the excerpt is protected with a password.', 'lifterlms' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'permalink' => array( + 'description' => __( 'Post URL.', 'lifterlms' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Post URL slug.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => array( $this, 'sanitize_slug' ), + ), + ), + 'post_type' => array( + 'description' => __( 'LifterLMS custom post type', 'lifterlms' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'The publication status of the post.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( + get_post_stati( + array( + '_builtin' => true, + 'internal' => false, + ) + ) + ), + 'context' => array( 'view', 'edit' ), + ), + 'password' => array( + 'description' => __( 'Password used to protect access to the content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'featured_media' => array( + 'description' => __( 'Featured image ID.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'comment_status' => array( + 'description' => __( 'Post comment status. Default comment status dependent upon general WordPress post discussion settings.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'open', + 'enum' => array( 'open', 'closed' ), + 'context' => array( 'view', 'edit' ), + ), + 'ping_status' => array( + 'description' => __( 'Post ping status. Default ping status dependent upon general WordPress post discussion settings.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'open', + 'enum' => array( 'open', 'closed' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + /** + * TODO: understand how to treat possible conflicting properties => instructors are registered as additional rest field by llms_blocks. + */ + // $schema = $this->add_additional_fields_schema( $schema ); + return $schema; + } + + /** + * Get object. + * + * @since 1.0.0-beta.9 + * + * @param int $id Object ID. + * @return LLMS_Course|WP_Error + */ + protected function get_object( $id ) { + + $class = $this->llms_post_class_from_post_type(); + + if ( ! $class ) { + return new WP_Error( + 'llms_rest_cannot_get_object', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be retrieved.', 'lifterlms' ), $this->post_type ), + array( 'status' => 500 ) + ); + } + + $object = llms_get_post( $id ); + return $object && is_a( $object, $class ) ? $object : llms_rest_not_found_error(); + } + + /** + * Create an LLMS_Post_Model + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 Implement generic llms post creation. + * + * @param array $object_args Object args. + * @return LLMS_Post_Model|WP_Error + */ + protected function create_llms_post( $object_args ) { + + $class = $this->llms_post_class_from_post_type(); + + if ( ! $class ) { + return new WP_Error( + 'llms_rest_cannot_create_object', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be created.', 'lifterlms' ), $this->post_type ), + array( 'status' => 500 ) + ); + } + + $object = new $class( 'new', $object_args ); + return $object && is_a( $object, $class ) ? $object : llms_rest_not_found_error(); + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`. + * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. + * @since 1.0.0-beta.7 `self` and `collection` links prepared in the parent class. + * Fix wp:featured_media link, we don't expose any embeddable field. + * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route. + * @since 1.0.0-beta.14 Added $request parameter. + * + * @param LLMS_Post_Model $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $object, $request ) { + + $links = parent::prepare_links( $object, $request ); + + $object_id = $object->get( 'id' ); + + // Content. + $links['content'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d/%s', $this->namespace, $this->rest_base, $object_id, 'content' ) ), + ); + + // If we have a featured media, add that. + $featured_media = get_post_thumbnail_id( $object_id ); + if ( $featured_media ) { + $image_url = rest_url( 'wp/v2/media/' . $featured_media ); + + $links['https://api.w.org/featuredmedia'] = array( + 'href' => $image_url, + ); + } + + $taxonomies = get_object_taxonomies( $this->post_type ); + + if ( ! empty( $taxonomies ) ) { + $links['https://api.w.org/term'] = array(); + + foreach ( $taxonomies as $tax ) { + $taxonomy_obj = get_taxonomy( $tax ); + + // Skip taxonomies that are not set to be shown in REST and LLMS REST. + if ( empty( $taxonomy_obj->show_in_rest ) || empty( $taxonomy_obj->show_in_llms_rest ) ) { + continue; + } + + $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax; + + $terms_url = add_query_arg( + 'post', + $object_id, + rest_url( 'wp/v2/' . $tax_base ) + ); + + $links['https://api.w.org/term'][] = array( + 'href' => $terms_url, + 'taxonomy' => $tax, + ); + } + } + + return $links; + + } + + /** + * Re-add filters previously removed + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object Object. + * @return array Array of filters removed for response. + */ + protected function maybe_remove_filters_for_response( $object ) { + + $filters_to_be_removed = $this->get_filters_to_be_removed_for_response( $object ); + $filters_removed = array(); + + // Need to remove some filters. + foreach ( $filters_to_be_removed as $hook => $filters ) { + foreach ( $filters as $filter_data ) { + $has_filter = has_filter( $hook, $filter_data['callback'] ); + + if ( false !== $has_filter && $filter_data['priority'] === $has_filter ) { + remove_filter( $hook, $filter_data['callback'], $filter_data['priority'] ); + if ( ! isset( $filters_removed[ $hook ] ) ) { + $filters_removed[ $hook ] = array(); + } + $filters_removed[ $hook ][] = $filter_data; + + } + } + } + + return $filters_removed; + + } + + /** + * Re-add filters previously removed + * + * @since 1.0.0-beta.1 + * + * @param array $filters_removed Array of filters removed to be re-added. + * @return void + */ + protected function maybe_add_removed_filters_for_response( $filters_removed ) { + + if ( ! empty( $filters_removed ) ) { + foreach ( $filters_removed as $hook => $filters ) { + foreach ( $filters as $filter_data ) { + add_filter( + $hook, + $filter_data['callback'], + $filter_data['priority'], + isset( $filter_data['accepted_args'] ) ? $filter_data['accepted_args'] : 1 + ); + } + } + } + } + + /** + * Get action/filters to be removed before preparing the item for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 Removed `"llms_rest_{$this->post_type}_filters_removed_for_reponse"` filter hooks, + * `"llms_rest_{$this->post_type}_filters_removed_for_response"` added. + * + * @param LLMS_Post_Model $object LLMS_Post_Model object. + * @return array Array of action/filters to be removed for response. + */ + protected function get_filters_to_be_removed_for_response( $object ) { + + /** + * Modify the array of filters to be removed before building the response. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @since 1.0.0-beta.9 + * + * @param array $filters Array of filters to be removed. + * @param LLMS_Post_Model $object LLMS_Post_Model object. + */ + return apply_filters( "llms_rest_{$this->post_type}_filters_removed_for_response", array(), $object ); + + } + + /** + * Determines validity and normalizes the given status parameter. + * Heavily based on WP_REST_Posts_Controller::handle_status_param(). + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use plural post type name. + * + * @param string $status Status. + * @return string|WP_Error Status or WP_Error if lacking the proper permission. + */ + protected function handle_status_param( $status ) { + + $post_type_object = get_post_type_object( $this->post_type ); + $post_type_name = $post_type_object->labels->name; + + switch ( $status ) { + case 'draft': + case 'pending': + break; + case 'private': + if ( ! current_user_can( $post_type_object->cap->publish_posts ) ) { + // Translators: %s = The post type name. + return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to create private %s.', 'lifterlms' ), $post_type_name ) ); + } + break; + case 'publish': + case 'future': + if ( ! current_user_can( $post_type_object->cap->publish_posts ) ) { + // Translators: $s = The post type name. + return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to publish %s.', 'lifterlms' ), $post_type_name ) ); + } + break; + default: + if ( ! get_post_status_object( $status ) ) { + $status = 'draft'; + } + break; + } + + return $status; + } + + /** + * Determines the featured media based on a request param + * + * Heavily based on WP_REST_Posts_Controller::handle_featured_media(). + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`. + * + * @param int $featured_media Featured Media ID. + * @param int $object_id LLMS object ID. + * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error. + */ + protected function handle_featured_media( $featured_media, $object_id ) { + + $featured_media = (int) $featured_media; + if ( $featured_media ) { + $result = set_post_thumbnail( $object_id, $featured_media ); + if ( $result ) { + return true; + } else { + return llms_rest_bad_request_error( __( 'Invalid featured media ID.', 'lifterlms' ) ); + } + } else { + return delete_post_thumbnail( $object_id ); + } + + } + + /** + * Updates the post's terms from a REST request. + * + * Heavily based on WP_REST_Posts_Controller::handle_terms(). + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`. + * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. + * + * @param int $object_id The post ID to update the terms form. + * @param WP_REST_Request $request The request object with post and terms data. + * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null. + */ + protected function handle_terms( $object_id, $request ) { + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_llms_rest' => true ) ); + + foreach ( $taxonomies as $taxonomy ) { + $base = $this->get_taxonomy_rest_base( $taxonomy ); + + if ( ! isset( $request[ $base ] ) ) { + continue; + } + + // We could use LLMS_Post_Model::set_terms() but it doesn't return a WP_Error which can be useful here. + $result = wp_set_object_terms( $object_id, $request[ $base ], $taxonomy->name ); + if ( is_wp_error( $result ) ) { + return $result; + } + } + } + + /** + * Checks whether current user can assign all terms sent with the current request. + * + * Heavily based on WP_REST_Posts_Controller::check_assign_terms_permission(). + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. + * + * @param WP_REST_Request $request The request object with post and terms data. + * @return bool Whether the current user can assign the provided terms. + */ + protected function check_assign_terms_permission( $request ) { + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_llms_rest' => true ) ); + foreach ( $taxonomies as $taxonomy ) { + $base = $this->get_taxonomy_rest_base( $taxonomy ); + + if ( ! isset( $request[ $base ] ) ) { + continue; + } + + foreach ( $request[ $base ] as $term_id ) { + // Invalid terms will be rejected later. + if ( ! get_term( $term_id, $taxonomy->name ) ) { + continue; + } + + if ( ! current_user_can( 'assign_term', (int) $term_id ) ) { + return false; + } + } + } + + return true; + } + + /** + * Maps a taxonomy name to the relative rest base + * + * @since 1.0.0-beta.1 + * + * @param object $taxonomy The taxonomy object. + * @return string The taxonomy rest base. + */ + protected function get_taxonomy_rest_base( $taxonomy ) { + + return ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + } + + /** + * Checks if a post can be edited. + * + * @since 1.0.0-beta.1 + * + * @return bool Whether the post can be created + */ + protected function check_create_permission() { + + $post_type = get_post_type_object( $this->post_type ); + return current_user_can( $post_type->cap->publish_posts ); + + } + + /** + * Checks if an llms post can be edited. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object Optional. The LLMS_Post_model object. Default null. + * @return bool Whether the post can be edited. + */ + protected function check_update_permission( $object = null ) { + + $post_type = get_post_type_object( $this->post_type ); + return is_null( $object ) ? current_user_can( $post_type->cap->edit_posts ) : current_user_can( $post_type->cap->edit_post, $object->get( 'id' ) ); + + } + + /** + * Checks if an llms post can be deleted. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object The LLMS_Post_model object. + * @return bool Whether the post can be deleted. + */ + protected function check_delete_permission( $object ) { + + $post_type = get_post_type_object( $this->post_type ); + return current_user_can( $post_type->cap->delete_post, $object->get( 'id' ) ); + + } + + /** + * Checks if an llms post can be read. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object The LLMS_Post_model object. + * @return bool Whether the post can be read. + */ + protected function check_read_permission( $object ) { + + $post_type = get_post_type_object( $this->post_type ); + $status = $object->get( 'status' ); + $id = $object->get( 'id' ); + $wp_post = $object->get( 'post' ); + + // Is the post readable? + if ( 'publish' === $status || current_user_can( $post_type->cap->read_post, $id ) ) { + return true; + } + + $post_status_obj = get_post_status_object( $status ); + if ( $post_status_obj && $post_status_obj->public ) { + return true; + } + + // Can we read the parent if we're inheriting? + if ( 'inherit' === $status && $wp_post->post_parent > 0 ) { + $parent = get_post( $wp_post->post_parent ); + if ( $parent ) { + return $this->check_read_permission( $parent ); + } + } + + /* + * If there isn't a parent, but the status is set to inherit, assume + * it's published (as per get_post_status()). + */ + if ( 'inherit' === $status ) { + return true; + } + + return false; + + } + + + /** + * Checks if the user can access password-protected content. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Post_Model $object The LLMS_Post_model object. + * @param WP_REST_Request $request Request data to check. + * @return bool True if the user can access password-protected content, otherwise false. + */ + public function can_access_password_content( $object, $request ) { + + if ( empty( $object->get( 'password' ) ) ) { + // No filter required. + return false; + } + + // Edit context always gets access to password-protected posts. + if ( 'edit' === $request['context'] ) { + return true; + } + + // No password, no auth. + if ( empty( $request['password'] ) ) { + return false; + } + + // Double-check the request password. + return hash_equals( $object->get( 'password' ), $request['password'] ); + } + + /** + * Get the llms post model class from the controller post type. + * + * @since 1.0.0-beta.9 + * + * @return string|bool The llms post model class name if it exists or FALSE if it doesn't. + */ + protected function llms_post_class_from_post_type() { + + if ( isset( $this->llms_post_class ) ) { + return $this->llms_post_class; + } + + $post_type = explode( '_', str_replace( 'llms_', '', $this->post_type ) ); + $class = 'LLMS'; + + foreach ( $post_type as $part ) { + $class .= '_' . ucfirst( $part ); + } + + if ( class_exists( $class ) ) { + $this->llms_post_class = $class; + } else { + $this->llms_post_class = false; + } + + return $this->llms_post_class; + } + + /** + * Sanitizes and validates the list of post statuses, including whether the user can query private statuses + * + * Heavily based on the WordPress WP_REST_Posts_Controller::sanitize_post_statuses(). + * + * @since 1.0.0-beta.19 + * + * @param string|array $statuses One or more post statuses. + * @param WP_REST_Request $request Full details about the request. + * @param string $parameter Additional parameter to pass to validation. + * @return array|WP_Error A list of valid statuses, otherwise WP_Error object. + */ + public function sanitize_post_statuses( $statuses, $request, $parameter ) { + $statuses = wp_parse_slug_list( $statuses ); + + $attributes = $request->get_attributes(); + $default_status = $attributes['args']['status']['default']; + + foreach ( $statuses as $status ) { + if ( $status === $default_status ) { + continue; + } + + $post_type_obj = get_post_type_object( $this->post_type ); + + if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) { + $result = rest_validate_request_arg( $status, $request, $parameter ); + if ( is_wp_error( $result ) ) { + return $result; + } + } else { + return llms_rest_authorization_required_error( __( 'Status is forbidden.', 'lifterlms' ) ); + } + } + + return $statuses; + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-users-controller.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-users-controller.php new file mode 100644 index 0000000000..066be87edd --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-users-controller.php @@ -0,0 +1,777 @@ +<?php +/** + * Base users controller class + * + * @package LifterLMS_REST/Abstracts + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Users_Controller class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added `check_read_object_permissions()` method override. + * @since 1.0.0-beta.10 Fixed setting roles instead of appending them when updating user. + * @since 1.0.0-beta.11 Correctly map request's `billing_postcode` param to `billing_zip` meta. + * @since 1.0.0-beta.12 Add `search` and `search_columns` collection filtering. + * @since 1.0.0-beta.14 Only add remapped keys to the response when the schema key is present in the expected response fields array. + */ +abstract class LLMS_REST_Users_Controller extends LLMS_Rest_Controller { + + /** + * Resource ID or Name + * + * For example: 'student' or 'instructor'. + * + * @var string + */ + protected $resource_name; + + /** + * Schema properties available for ordering the collection + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'email', + 'name', + 'registered_date', + ); + + /** + * Whether search is allowed + * + * @var boolean + */ + protected $is_searchable = true; + + /** + * Schema properties to query search columns mapping + * + * @var array + */ + protected $search_columns_mapping = array( + 'id' => 'ID', + 'username' => 'user_login', + 'email' => 'user_email', + 'url' => 'user_url', + 'name' => 'display_name', + ); + + /** + * Determine if the current user has permissions to manage the role(s) present in a request + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + protected function check_roles_permissions( $request ) { + + global $wp_roles; + + $schema = $this->get_item_schema(); + $roles = array(); + if ( ! empty( $request['roles'] ) ) { + $roles = $request['roles']; + } elseif ( ! empty( $schema['properties']['roles']['default'] ) ) { + $roles = $schema['properties']['roles']['default']; + } + + foreach ( $roles as $role ) { + + if ( ! isset( $wp_roles->role_objects[ $role ] ) ) { + // Translators: %s = role key. + return llms_rest_bad_request_error( sprintf( __( 'The role %s does not exist.', 'lifterlms' ), $role ) ); + } + + $potential_role = $wp_roles->role_objects[ $role ]; + + /* + * Don't let anyone with 'edit_users' (admins) edit their own role to something without it. + * Multisite super admins can freely edit their blog roles -- they possess all caps. + */ + if ( ! ( is_multisite() + && current_user_can( 'manage_sites' ) ) + && get_current_user_id() === $request['id'] + && ! $potential_role->has_cap( 'edit_users' ) + ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to give users this role.', 'lifterlms' ) ); + } + + // Include admin functions to get access to `get_editable_roles()`. + require_once ABSPATH . 'wp-admin/includes/admin.php'; + + // The new role must be editable by the logged-in user. + $editable_roles = get_editable_roles(); + + if ( empty( $editable_roles[ $role ] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to give users this role.', 'lifterlms' ) ); + } + } + + return true; + + } + + /** + * Insert the prepared data into the database + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from `$this->get_object()`. + */ + protected function create_object( $prepared, $request ) { + + $object_id = wp_insert_user( $prepared ); + + if ( is_wp_error( $object_id ) ) { + return $object_id; + } + + return $this->update_additional_data( $object_id, $prepared, $request ); + + } + + + /** + * Delete the object + * + * Note: we do not return 404s when the resource to delete cannot be found. We assume it's already been deleted and respond with 204. + * Errors returned by this method should be any error other than a 404! + * + * @since 1.0.0-beta.1 + * + * @param obj $object Instance of the object from `$this->get_object()`. + * @param WP_REST_Request $request Request object. + * @return true|WP_Error `true` when the object is removed, `WP_Error` on failure. + */ + protected function delete_object( $object, $request ) { + + $id = $object->get( 'id' ); + $reassign = 0 === $request['reassign'] ? null : $request['reassign']; + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return llms_rest_bad_request_error( __( 'Invalid user ID for reassignment.', 'lifterlms' ) ); + } + } + + // Include admin user functions to get access to `wp_delete_user()`. + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $result = wp_delete_user( $id, $reassign ); + + if ( ! $result ) { + return llms_rest_server_error( __( 'The user could not be deleted.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Determine if the current user can view the object + * + * @since 1.0.0-beta.7 + * + * @param object $object Object. + * @return bool + */ + protected function check_read_object_permissions( $object ) { + return $this->check_read_item_permissions( $this->get_object_id( $object ) ); + } + + /** + * Retrieves the query params for the objects collection + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $params = parent::get_collection_params(); + + $params['roles'] = array( + 'description' => __( 'Include only users keys matching matching a specific role. Accepts a single role or a comma separated list of roles.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $this->get_enum_roles(), + ), + ); + + return $params; + + } + + /** + * Retrieve arguments for deleting a resource + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_delete_item_args() { + return array( + 'reassign' => array( + 'type' => 'integer', + 'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.', 'lifterlms' ), + 'default' => 0, + 'sanitize_callback' => 'absint', + ), + ); + } + + /** + * Retrieve an array of allowed user role values + * + * @since 1.0.0-beta.1 + * + * @return string[] + */ + protected function get_enum_roles() { + + global $wp_roles; + return array_keys( $wp_roles->roles ); + + } + + /** + * Get the item schema + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_item_schema() { + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->resource_name, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the user.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Login name for the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => array( $this, 'sanitize_username' ), + ), + ), + 'name' => array( + 'description' => __( 'Display name for the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'first_name' => array( + 'description' => __( 'First name for the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Last name for the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'email' => array( + 'description' => __( 'The email address for the user.', 'lifterlms' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'edit' ), + 'required' => true, + ), + 'url' => array( + 'description' => __( 'URL of the user.', 'lifterlms' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Description of the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'nickname' => array( + 'description' => __( 'The nickname for the user.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'registered_date' => array( + 'description' => __( 'Registration date for the user.', 'lifterlms' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'roles' => array( + 'description' => __( 'Roles assigned to the user.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $this->get_enum_roles(), + ), + 'context' => array( 'edit' ), + 'default' => array( 'student' ), + ), + 'password' => array( + 'description' => __( 'Password for the user (never included).', 'lifterlms' ), + 'type' => 'string', + 'context' => array(), // Password is never displayed. + 'arg_options' => array( + 'sanitize_callback' => array( $this, 'sanitize_password' ), + ), + ), + 'billing_address_1' => array( + 'description' => __( 'User address line 1.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'billing_address_2' => array( + 'description' => __( 'User address line 2.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'billing_city' => array( + 'description' => __( 'User address city name.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'billing_state' => array( + 'description' => __( 'User address ISO code for the state, province, or district.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'billing_postcode' => array( + 'description' => __( 'User address postal code.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'billing_country' => array( + 'description' => __( 'User address ISO code for the country.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + if ( get_option( 'show_avatars' ) ) { + + $avatar_properties = array(); + foreach ( rest_get_avatar_sizes() as $size ) { + $avatar_properties[ $size ] = array( + // Translators: %d = avatar image size in pixels. + 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'lifterlms' ), $size ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ); + } + + $schema['properties']['avatar_urls'] = array( + 'description' => __( 'Avatar URLs for the user.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $avatar_properties, + ); + + } + + return $schema; + + } + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Parse `search` and `search_columns` args. + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return WP_User_Query + */ + protected function get_objects_query( $prepared, $request ) { + + if ( 'id' === $prepared['orderby'] ) { + $prepared['orderby'] = 'ID'; + } elseif ( 'registered_date' === $prepared['orderby'] ) { + $prepared['orderby'] = 'registered'; + } + + $args = array( + 'paged' => $prepared['page'], + 'number' => $prepared['per_page'], + 'order' => strtoupper( $prepared['order'] ), + 'orderby' => $prepared['orderby'], + ); + + if ( ! empty( $prepared['roles'] ) ) { + $args['role__in'] = $prepared['roles']; + } + + if ( ! empty( $prepared['include'] ) ) { + $args['include'] = $prepared['include']; + } + + if ( ! empty( $prepared['exclude'] ) ) { + $args['exclude'] = $prepared['exclude']; + } + + if ( ! empty( $prepared['search'] ) ) { + $args['search'] = $prepared['search']; + } + + if ( ! empty( $prepared['search_columns'] ) ) { + $args['search_columns'] = $prepared['search_columns']; + } + + return new WP_User_Query( $args ); + + } + + + /** + * Retrieve an array of objects from the result of `$this->get_objects_query()` + * + * @since 1.0.0-beta.1 + * + * @param obj $query Objects query result. + * @return WP_User[] + */ + protected function get_objects_from_query( $query ) { + return $query->get_results(); + } + + /** + * Retrieve pagination information from an objects query + * + * @since 1.0.0-beta.1 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + $current_page = absint( $prepared['page'] ); + $total_results = $query->get_total(); + $total_pages = absint( ceil( $total_results / $prepared['per_page'] ) ); + + return compact( 'current_page', 'total_results', 'total_pages' ); + + } + + /** + * Map request keys to database keys for insertion + * + * Array keys are the request fields (as defined in the schema) and + * array values are the database fields. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.11 Correctly map request's `billing_postcode` param to `billing_zip` meta. + * + * @return array + */ + protected function map_schema_to_database() { + + $map = parent::map_schema_to_database(); + + $map['username'] = 'user_login'; + $map['password'] = 'user_pass'; + $map['name'] = 'display_name'; + $map['email'] = 'user_email'; + $map['url'] = 'user_url'; + $map['registered_date'] = 'user_registered'; + $map['billing_postcode'] = 'billing_zip'; + + // Not inserted/read via database calls. + unset( $map['roles'], $map['avatar_urls'] ); + + return $map; + + } + + /** + * Prepare request arguments for a database insert/update + * + * @since 1.0.0-beta.1 + * + * @param WP_Rest_Request $request Request object. + * @return array + */ + protected function prepare_item_for_database( $request ) { + + $prepared = parent::prepare_item_for_database( $request ); + + // If we're creating a new item, maybe add some defaults. + if ( empty( $prepared['id'] ) ) { + + // Pass an explicit false to `wp_insert_user()`. + $prepared['role'] = false; + + if ( empty( $prepared['user_pass'] ) ) { + $prepared['user_pass'] = wp_generate_password( 22 ); + } + + if ( empty( $prepared['user_login'] ) ) { + $prepared['user_login'] = LLMS_Person_Handler::generate_username( $prepared['user_email'] ); + } + } + + return $prepared; + + } + + /** + * Prepare an object for response + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Only add remapped keys to the response when the schema key is present in the expected response fields array. + * + * @param LLMS_Abstract_User_Data $object User object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_object_for_response( $object, $request ) { + + $prepared = array(); + $map = array_flip( $this->map_schema_to_database() ); + $fields = $this->get_fields_for_response( $request ); + + // Write Only. + unset( $map['user_pass'] ); + + foreach ( $map as $db_key => $schema_key ) { + if ( in_array( $schema_key, $fields, true ) ) { + $prepared[ $schema_key ] = $object->get( $db_key ); + } + } + + if ( in_array( 'roles', $fields, true ) ) { + $prepared['roles'] = $object->get_user()->roles; + } + + if ( in_array( 'avatar_urls', $fields, true ) ) { + $prepared['avatar_urls'] = rest_get_avatar_urls( $object->get( 'user_email' ) ); + } + + return $prepared; + + } + + /** + * Validate a username is valid and allowed + * + * @since 1.0.0-beta.1 + * + * @param string $value User-submitted username. + * @param WP_REST_Request $request Request object. + * @param string $param Parameter name. + * @return WP_Error|string Sanitized username if valid or error object. + */ + public function sanitize_password( $value, $request, $param ) { + + $password = (string) $value; + + if ( false !== strpos( $password, '\\' ) ) { + return llms_rest_bad_request_error( __( 'Passwords cannot contain the "\\" character.', 'lifterlms' ) ); + } + + // @todo: Should validate against password strength too, maybe? + + return $password; + + } + + /** + * Validate a username is valid and allowed + * + * @since 1.0.0-beta.1 + * + * @param string $value User-submitted username. + * @param WP_REST_Request $request Request object. + * @param string $param Parameter name. + * @return WP_Error|string Sanitized username if valid or error object. + */ + public function sanitize_username( $value, $request, $param ) { + + $username = (string) $value; + + if ( ! validate_username( $username ) ) { + return llms_rest_bad_request_error( __( 'Username contains invalid characters.', 'lifterlms' ) ); + } + + /** + * Filter defined in WP Core. + * + * @link https://developer.wordpress.org/reference/hooks/illegal_user_logins/ + * + * @param array $illegal_logins Array of banned usernames. + */ + $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); + if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { + return llms_rest_bad_request_error( __( 'Username is not allowed.', 'lifterlms' ) ); + } + + return $username; + + } + + /** + * Updates additional information not handled by WP Core insert/update user functions + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Fixed setting roles instead of appending them. + * @since 1.0.0-beta.11 Made sure to set user's meta with the correct db key. + * + * @param int $object_id WP User id. + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return LLMS_Abstract_User_Data|WP_error + */ + protected function update_additional_data( $object_id, $prepared, $request ) { + + $object = $this->get_object( $object_id ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $metas = array( + 'billing_address_1', + 'billing_address_2', + 'billing_city', + 'billing_state', + 'billing_postcode', + 'billing_country', + ); + + $map = $this->map_schema_to_database(); + + foreach ( $metas as $meta ) { + if ( ! empty( $map[ $meta ] ) && ! empty( $prepared[ $map[ $meta ] ] ) ) { + $object->set( $map[ $meta ], $prepared[ $map[ $meta ] ] ); + } + } + + if ( ! empty( $request['roles'] ) ) { + $user = $object->get_user(); + $user->set_role( '' ); + foreach ( $request['roles'] as $role ) { + $user->add_role( $role ); + } + } + + return $object; + + } + + /** + * Update item + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object or `WP_Error` on failure. + */ + public function update_item( $request ) { + + $object = $this->get_object( $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + // Ensure we're not trying to update the email to an email that already exists. + $owner_id = email_exists( $request['email'] ); + + if ( $owner_id && $owner_id !== $request['id'] ) { + return llms_rest_bad_request_error( __( 'Invalid email address.', 'lifterlms' ) ); + } + + // Cannot change a username. + if ( ! empty( $request['username'] ) && $request['username'] !== $object->get( 'user_login' ) ) { + return llms_rest_bad_request_error( __( 'Username is not editable.', 'lifterlms' ) ); + } + + return parent::update_item( $request ); + + } + + /** + * Update the object in the database with prepared data + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from `$this->get_object()`. + */ + protected function update_object( $prepared, $request ) { + + $prepared['ID'] = $prepared['id']; + + $object_id = wp_update_user( $prepared ); + if ( is_wp_error( $object_id ) ) { + return $object_id; + } + + unset( $prepared['ID'] ); + + return $this->update_additional_data( $object_id, $prepared, $request ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-webhook-data.php b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-webhook-data.php new file mode 100644 index 0000000000..b064d9c743 --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/class-llms-rest-webhook-data.php @@ -0,0 +1,323 @@ +<?php +/** + * Webhook Getters & Setters + * + * @package LifterLMS_REST/Abstracts + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.17 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Webhook class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.6 Retrieve proper payload for enrollment and progress resources. + * @since 1.0.0-beta.17 Remove unused 'pending_delivery' column. + */ +abstract class LLMS_REST_Webhook_Data extends LLMS_Abstract_Database_Store { + + /** + * Array of table column name => format + * + * @var string[] + */ + protected $columns = array( + + 'status' => '%s', + 'name' => '%s', + 'delivery_url' => '%s', + 'secret' => '%s', + 'topic' => '%s', + 'user_id' => '%d', + 'created' => '%s', + 'updated' => '%s', + 'failure_count' => '%d', + + ); + + /** + * Database Table Name + * + * @var string + */ + protected $table = 'webhooks'; + + /** + * The record type + * + * Used for filters/actions. + * + * @var string + */ + protected $type = 'webhook'; + + /** + * Constructor + * + * @since 1.0.0-beta.1 + * + * @param int $id API Key ID. + * @param bool $hydrate If true, hydrates the object on instantiation if an ID is supplied. + */ + public function __construct( $id = null, $hydrate = true ) { + + $this->id = $id; + if ( $this->id && $hydrate ) { + $this->hydrate(); + } + + // Adds created and updated dates on instantiation. + parent::__construct(); + + } + + + /** + * Retrieve an admin nonce url for deleting an API key. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_delete_link() { + + return add_query_arg( + array( + 'section' => 'webhooks', + 'delete-webhook' => $this->get( 'id' ), + 'delete-webhook-nonce' => wp_create_nonce( 'delete' ), + ), + LLMS_REST_API()->keys()->get_admin_url() + ); + + } + + /** + * Generate a delivery signature from a delivery payload. + * + * @since 1.0.0-beta.1 + * + * @param string $payload JSON-encoded payload. + * @return string + */ + public function get_delivery_signature( $payload ) { + + /** + * Allow overriding of signature generation. + * + * @since 1.0.0-beta.1 + * + * @param string $signature Custom signature. Return a string to replace the default signature. + * @param string $payload JSON-encoded body to be delivered. + * @param int $id Webhook id. + */ + $signature = apply_filters( 'llms_rest_webhook_signature_pre', null, $payload, $this->get( 'id' ) ); + if ( $signature && is_string( $signature ) ) { + return $signature; + } + + /** + * Customize the hash algorithm used to generate the webhook delivery signature. + * + * @since 1.0.0-beta.1 + * + * @param string $algo Hash algorithm. Defaults to 'sha256'. List of supported algorithms available at https://www.php.net/manual/en/function.hash-hmac-algos.php. + * @param string $payload JSON-encoded body to be delivered. + * @param int $id Webhook ID. + */ + $hash_algo = apply_filters( 'llms_rest_webhook_hash_algorithm', 'sha256', $payload, $this->get( 'id' ) ); + $ts = llms_current_time( 'timestamp' ); + $message = $ts . '.' . $payload; + $hash = hash_hmac( $hash_algo, $message, $this->get( 'secret' ) ); + + return sprintf( 't=%1$d,v1=%2$s', $ts, $hash ); + + } + + /** + * Retrieve the admin URL where the api key is managed. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_edit_link() { + return add_query_arg( + array( + 'section' => 'webhooks', + 'edit-webhook' => $this->get( 'id' ), + ), + LLMS_REST_API()->keys()->get_admin_url() + ); + } + + /** + * Retrieve the topic event + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_event() { + + $topic = explode( '.', $this->get( 'topic' ) ); + return apply_filters( 'llms_rest_webhook_get_event', isset( $topic[1] ) ? $topic[1] : '', $this->get( 'id' ) ); + + } + + /** + * Retrieve an array of hooks for the webhook topic. + * + * @since 1.0.0-beta.1 + * + * @return string[] + */ + public function get_hooks() { + + if ( 'action' === $this->get_resource() ) { + $hooks = array( $this->get_event() => 1 ); + } else { + $all_hooks = LLMS_REST_API()->webhooks()->get_hooks(); + $topic = $this->get( 'topic' ); + $hooks = isset( $all_hooks[ $topic ] ) ? $all_hooks[ $topic ] : array(); + } + + return apply_filters( 'llms_rest_webhook_get_hooks', $hooks, $this->get( 'id' ) ); + + } + + /** + * Retrieve a payload for webhook delivery. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.6 Retrieve proper payload for enrollment and progress resources. + * + * @param array $args Numeric array of arguments from the originating hook. + * @return array + */ + protected function get_payload( $args ) { + + // Switch current user to the user who created the webhook. + $current_user = get_current_user_id(); + wp_set_current_user( $this->get( 'user_id' ) ); + + $resource = $this->get_resource(); + $event = $this->get_event(); + + $payload = array(); + if ( 'deleted' === $event ) { + + if ( in_array( $this->get_resource(), array( 'enrollment', 'progress' ), true ) ) { + $payload['student_id'] = $args[0]; + $payload['post_id'] = $args[1]; + } else { + $payload['id'] = $args[0]; + } + } elseif ( 'action' === $resource ) { + + $payload['action'] = current( $this->get_hooks() ); + $payload['args'] = $args; + + } else { + + if ( 'enrollment' === $resource ) { + $endpoint = sprintf( '/llms/v1/students/%1$d/enrollments/%2$d', $args[0], $args[1] ); + } elseif ( 'progress' === $resource ) { + $endpoint = sprintf( '/llms/v1/students/%1$d/progress/%2$d', $args[0], $args[1] ); + } else { + $endpoint = sprintf( '/llms/v1/%1$ss/%2$d', $resource, $args[0] ); + } + + $payload = llms_rest_get_api_endpoint_data( $endpoint ); + + } + + // Restore the current user. + wp_set_current_user( $current_user ); + + /** + * Filter the webhook payload prior to delivery + * + * @since 1.0.0-beta.1 + * + * @param array $payload Webhook payload. + * @param string $resource Webhook resource. + * @param string $event Webhook event. + * @param array $args Numeric array of arguments from the originating hook. + * @param LLMS_REST_Webhook $this Webhook object. + */ + return apply_filters( 'llms_rest_webhook_get_payload', $payload, $resource, $event, $args, $this ); + + } + + /** + * Retrieve the topic resource. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_resource() { + + $topic = explode( '.', $this->get( 'topic' ) ); + return apply_filters( 'llms_rest_webhook_get_resource', $topic[0], $this->get( 'id' ) ); + + } + + /** + * Retrieve a user agent string to use for delivering webhooks. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + protected function get_user_agent() { + global $wp_version; + return sprintf( 'LifterLMS/%1$s Hookshot (WordPress/%2$s)', LLMS()->version, $wp_version ); + } + + /** + * Increment delivery failures and after max allowed failures are reached, set status to disabled. + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_Webhook + */ + protected function set_delivery_failure() { + + $failures = absint( $this->get( 'failure_count' ) ); + + $this->set( 'failure_count', ++$failures ); + + /** + * Filter the number of times a webhook is allowed to fail before it is automatically disabled. + * + * @since 1.0.0-beta.1 + * + * @param int $num Number of allowed failures. Default: 5. + */ + $max_allowed = apply_filters( 'llms_rest_webhook_max_delivery_failures', 5 ); + + if ( $failures > $max_allowed ) { + + $this->set( 'status', 'disabled' ); + + /** + * Fires immediately after a webhook has been disabled due to exceeding its maximum allowed failures. + * + * @since 1.0.0-beta.1 + * + * @param int $webhook_id ID of the webhook. + */ + do_action( 'llms_rest_webhook_disabled_by_delivery_failures', $this->get( 'id' ) ); + + } + + return $this; + + } + +} diff --git a/libraries/lifterlms-rest/includes/abstracts/index.php b/libraries/lifterlms-rest/includes/abstracts/index.php new file mode 100644 index 0000000000..9c65c1efa6 --- /dev/null +++ b/libraries/lifterlms-rest/includes/abstracts/index.php @@ -0,0 +1 @@ +<?php // shhh. diff --git a/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-form-controller.php b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-form-controller.php new file mode 100644 index 0000000000..6f4015022c --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-form-controller.php @@ -0,0 +1,188 @@ +<?php +/** + * Handle admin form submissions. + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.3 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Admin_Form_Controller class.. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Added API credential download methods. + */ +class LLMS_REST_Admin_Form_Controller { + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function __construct() { + + add_action( 'admin_init', array( $this, 'handle_events' ) ); + + } + + /** + * Handles submission of admin forms & nonce links. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Added logic for handling api key txt download via nonce link. + * + * @return false|void + */ + public function handle_events() { + + if ( llms_verify_nonce( 'key-revoke-nonce', 'revoke', 'GET' ) ) { + $delete = LLMS_REST_API()->keys()->delete( llms_filter_input( INPUT_GET, 'revoke-key', FILTER_VALIDATE_INT ) ); + if ( $delete ) { + LLMS_Admin_Notices::flash_notice( esc_html__( 'The API Key has been successfully deleted.', 'lifterlms' ), 'success' ); + return llms_redirect_and_exit( admin_url( 'admin.php?page=llms-settings&tab=rest-api§ion=keys' ) ); + } + } elseif ( llms_verify_nonce( 'llms_rest_webhook_nonce', 'create-update-webhook', 'POST' ) ) { + return $this->handle_webhook_upsert(); + } elseif ( llms_verify_nonce( 'delete-webhook-nonce', 'delete', 'GET' ) ) { + $delete = LLMS_REST_API()->webhooks()->delete( llms_filter_input( INPUT_GET, 'delete-webhook', FILTER_VALIDATE_INT ) ); + if ( $delete ) { + LLMS_Admin_Notices::flash_notice( esc_html__( 'The webhook has been successfully deleted.', 'lifterlms' ), 'success' ); + return llms_redirect_and_exit( admin_url( 'admin.php?page=llms-settings&tab=rest-api§ion=webhooks' ) ); + } + } elseif ( llms_verify_nonce( 'dl-key-nonce', 'dl-key', 'GET' ) ) { + return $this->handle_key_download(); + } + + return false; + + } + + /** + * Generate and download a api key credentials file. + * + * @since 1.0.0-beta.3 + * + * @return false|void + */ + protected function handle_key_download() { + + $info = $this->prepare_key_download(); + if ( ! $info ) { + return false; + } + + header( 'Content-type: text/plain' ); + header( 'Content-Disposition: attachment; filename="' . $info['fn'] ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + + // Translators: %s = Consumer Key. + printf( __( 'Consumer Key: %s', 'lifterlms' ), $info['ck'] ); + echo "\r\n"; + // Translators: %s = Consumer Secret. + printf( __( 'Consumer Secret: %s', 'lifterlms' ), $info['cs'] ); + die(); + + } + + /** + * Handle creating/updating a webhook via admin interfaces + * + * @since 1.0.0-beta.1 + * + * @return true|void|WP_Error true on update success, void (redirect) on creation success, WP_Error on failure. + */ + protected function handle_webhook_upsert() { + + $data = array( + 'name' => llms_filter_input( INPUT_POST, 'llms_rest_webhook_name', FILTER_SANITIZE_STRING ), + 'status' => llms_filter_input( INPUT_POST, 'llms_rest_webhook_status', FILTER_SANITIZE_STRING ), + 'topic' => llms_filter_input( INPUT_POST, 'llms_rest_webhook_topic', FILTER_SANITIZE_STRING ), + 'delivery_url' => llms_filter_input( INPUT_POST, 'llms_rest_webhook_delivery_url', FILTER_SANITIZE_URL ), + 'secret' => llms_filter_input( INPUT_POST, 'llms_rest_webhook_secret', FILTER_SANITIZE_STRING ), + ); + + if ( 'action' === $data['topic'] ) { + $data['topic'] .= '.' . llms_filter_input( INPUT_POST, 'llms_rest_webhook_action', FILTER_SANITIZE_STRING ); + } + + $hook_id = llms_filter_input( INPUT_POST, 'llms_rest_webhook_id', FILTER_SANITIZE_NUMBER_INT ); + + if ( ! $hook_id ) { + + $hook = LLMS_REST_API()->webhooks()->create( $data ); + if ( ! is_wp_error( $hook ) ) { + return llms_redirect_and_exit( $hook->get_edit_link(), array( 'status' => 301 ) ); + } + } else { + + $hook = LLMS_REST_API()->webhooks()->get( $hook_id ); + if ( ! $hook ) { + + // Translators: %s = Webhook ID. + $hook = new WP_Error( 'llms_rest_api_webhook_not_found', sprintf( __( '"%s" is not a valid Webhook.', 'lifterlms' ), $hook_id ) ); + + } else { + + $data['id'] = $hook_id; + $hook = LLMS_REST_API()->webhooks()->update( $data ); + + } + } + + if ( is_wp_error( $hook ) ) { + // Translators: %1$s = error message; %2$s = error code. + LLMS_Admin_Notices::flash_notice( sprintf( __( 'Error: %1$s [Code: %2$s]', 'lifterlms' ), $hook->get_error_message(), $hook->get_error_code() ), 'error' ); + return $hook; + } + + return true; + + } + + /** + * Validates `GET` information from the credential download URL and prepares information for generating the file. + * + * @since 1.0.0-beta.3 + * + * @return false|array + */ + protected function prepare_key_download() { + + $key_id = llms_filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ); + $consumer_key = llms_filter_input( INPUT_GET, 'ck', FILTER_SANITIZE_STRING ); + + // return if missing required fields. + if ( ! $key_id || ! $consumer_key ) { + return false; + } + + // return if key doesn't exist. + $key = LLMS_REST_API()->keys()->get( $key_id ); + if ( ! $key ) { + return false; + } + + // validate the decoded consumer key looks like the stored truncated key. + $consumer_key = base64_decode( $consumer_key ); //phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- This is benign usage. + if ( substr( $consumer_key, -7 ) !== $key->get( 'truncated_key' ) ) { + return false; + } + + return array( + 'fn' => sanitize_file_name( $key->get( 'description' ) ) . '.txt', + 'ck' => $consumer_key, + 'cs' => $key->get( 'consumer_secret' ), + ); + + } + +} + +return new LLMS_REST_Admin_Form_Controller(); diff --git a/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-api-keys.php b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-api-keys.php new file mode 100644 index 0000000000..e367dcf027 --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-api-keys.php @@ -0,0 +1,311 @@ +<?php +/** + * Admin Settings Page: REST API + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.3 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Admin Settings Page: REST API + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Improve UX of key generation and updates. + */ +class LLMS_Rest_Admin_Settings_API_Keys { + + /** + * Holds an LLMS_REST_API_Key instance when a new key is generated. + * + * Used to show consumer key & secret one time immediately following creation. + * + * @var null + */ + private static $generated_key = null; + + /** + * Get settings fields for the Keys tab. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Add "required" to the description field, add helper text, & add credential download option after generation. + * + * @return array + */ + public static function get_fields() { + + require_once 'tables/class-llms-rest-table-api-keys.php'; + + $add_key = '1' === llms_filter_input( INPUT_GET, 'add-key', FILTER_SANITIZE_NUMBER_INT ); + $key_id = llms_filter_input( INPUT_GET, 'edit-key', FILTER_SANITIZE_NUMBER_INT ); + + $settings = array(); + + $settings[] = array( + 'class' => 'top', + 'id' => 'rest_keys_options_start', + 'type' => 'sectionstart', + ); + + $settings[] = array( + 'title' => $key_id || $add_key ? __( 'API Key Details', 'lifterlms' ) : __( 'API Keys', 'lifterlms' ), + 'type' => 'title-with-html', + 'id' => 'rest_keys_options_title', + 'html' => $key_id || $add_key ? '' : '<a href="' . esc_url( admin_url( 'admin.php?page=llms-settings&tab=rest-api§ion=keys&add-key=1' ) ) . '" class="llms-button-primary small" type="submit" style="top:-2px;">' . __( 'Add API Key', 'lifterlms' ) . '</a>', + ); + + if ( $add_key || $key_id ) { + + $key = $add_key ? false : new LLMS_REST_API_Key( $key_id ); + if ( self::$generated_key ) { + $key = self::$generated_key; + } + if ( $add_key || $key->exists() ) { + + $user_id = $key ? $key->get( 'user_id' ) : get_current_user_id(); + + $settings[] = array( + 'title' => __( 'Description', 'lifterlms' ), + 'desc' => '<br>' . __( 'A friendly, human-readable, name used to identify the key.', 'lifterlms' ), + 'id' => 'llms_rest_key_description', + 'type' => 'text', + 'value' => $key ? $key->get( 'description' ) : '', + 'custom_attributes' => array( + 'required' => 'required', + ), + ); + + $settings[] = array( + 'title' => __( 'User', 'lifterlms' ), + 'class' => 'llms-select2-student', + 'desc' => sprintf( + // Translators: %1$s = opening anchor tag to capabilities doc; %2$s closing anchor tag. + __( 'The owner is used to determine what user %1$scapabilities%2$s are available to the API key.', 'lifterlms' ), + '<a href="https://lifterlms.com/docs/roles-and-capabilities/" target="_blank">', + '</a>' + ), + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select a user', 'lifterlms' ), + ), + 'id' => 'llms_rest_key_user_id', + 'options' => llms_make_select2_student_array( array( $user_id ) ), + 'type' => 'select', + ); + + $settings[] = array( + 'title' => __( 'Permissions', 'lifterlms' ), + 'desc' => '<br>' . sprintf( + // Translators: %1$s = opening anchor tag to doc; %2$s closing anchor tag. + __( 'Determines what kind of requests can be made with the API key. %1$sRead more%2$s.', 'lifterlms' ), + '<a href="https://lifterlms.com/docs/getting-started-with-the-lifterlms-rest-api/#api-keys" target="_blank">', + '</a>' + ), + 'id' => 'llms_rest_key_permissions', + 'type' => 'select', + 'options' => LLMS_REST_API()->keys()->get_permissions(), + 'value' => $key ? $key->get( 'permissions' ) : '', + ); + + if ( $key && ! self::$generated_key ) { + + $settings[] = array( + 'title' => __( 'Consumer key ending in', 'lifterlms' ), + 'custom_attributes' => array( + 'readonly' => 'readonly', + ), + 'class' => 'code', + 'id' => 'llms_rest_key__read_only_key', + 'type' => 'text', + 'value' => '…' . $key->get( 'truncated_key' ), + ); + + $settings[] = array( + 'title' => __( 'Last accessed at', 'lifterlms' ), + 'custom_attributes' => array( + 'readonly' => 'readonly', + ), + 'id' => 'llms_rest_key__read_only_date', + 'type' => 'text', + 'value' => $key->get_last_access_date(), + ); + + } elseif ( self::$generated_key ) { + + $settings[] = array( + 'type' => 'custom-html', + 'id' => 'llms_rest_key_onetime_notice', + 'value' => '<p style="padding: 10px;border-left:4px solid #ff922b;background:rgba(255, 146, 43, 0.3);">' . __( 'Make sure to copy or download the consumer key and consumer secret. After leaving this page they will not be displayed again.', 'lifterlms' ) . '</p>', + ); + + $settings[] = array( + 'title' => __( 'Consumer key', 'lifterlms' ), + 'custom_attributes' => array( + 'readonly' => 'readonly', + ), + 'css' => 'width:400px', + 'class' => 'code widefat', + 'id' => 'llms_rest_key__read_only_key', + 'type' => 'text', + 'value' => $key->get( 'consumer_key_one_time' ), + ); + + $settings[] = array( + 'title' => __( 'Consumer secret', 'lifterlms' ), + 'custom_attributes' => array( + 'readonly' => 'readonly', + ), + 'css' => 'width:400px', + 'class' => 'code widefat', + 'id' => 'llms_rest_key__read_only_secret', + 'type' => 'text', + 'value' => $key->get( 'consumer_secret' ), + ); + + } + + $buttons = '<br><br>'; + if ( self::$generated_key ) { + $download_url = wp_nonce_url( + admin_url( + add_query_arg( + array( + 'id' => $key->get( 'id' ), + 'ck' => base64_encode( $key->get( 'consumer_key_one_time' ) ), //phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- This is benign usage. + ), + 'admin.php' + ) + ), + 'dl-key', + 'dl-key-nonce' + ); + $buttons .= '<a class="llms-button-primary" href="' . $download_url . '" target="_blank"><i class="fa fa-download" aria-hidden="true"></i> ' . __( 'Download Keys', 'lifterlms' ) . '</a>'; + } else { + $buttons .= '<button class="llms-button-primary" type="submit" value="llms-rest-save-key">' . __( 'Save', 'lifterlms' ) . '</button>'; + } + if ( $key ) { + $buttons .= $buttons ? '   ' : '<br><br>'; + $buttons .= '<a class="llms-button-danger" href="' . esc_url( $key->get_delete_link() ) . '">' . __( 'Revoke', 'lifterlms' ) . '</a>'; + } + $buttons .= wp_nonce_field( 'lifterlms-settings', '_wpnonce', true, false ); + + $settings[] = array( + 'type' => 'custom-html', + 'id' => 'llms_rest_key_buttons', + 'value' => $buttons, + ); + + } else { + + $settings[] = array( + 'id' => 'rest_keys_options_invalid_error', + 'type' => 'custom-html', + 'value' => __( 'Invalid api key.', 'lifterlms' ), + ); + + } + } else { + + $settings[] = array( + 'id' => 'llms_api_keys_table', + 'table' => new LLMS_REST_Table_API_Keys(), + 'type' => 'table', + ); + + } + + $settings[] = array( + 'id' => 'rest_keys_options_end', + 'type' => 'sectionend', + ); + + return $settings; + + } + + /** + * Form handler to save Create / Update an API key. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Remove key copy message in favor of message directly above the key fields. + * + * @return null|LLMS_REST_API_Key|WP_Error + */ + public static function save() { + + $ret = null; + + $key_id = llms_filter_input( INPUT_GET, 'edit-key', FILTER_SANITIZE_NUMBER_INT ); + if ( $key_id ) { + $ret = self::save_update( $key_id ); + } elseif ( llms_filter_input( INPUT_GET, 'add-key', FILTER_SANITIZE_NUMBER_INT ) ) { + $ret = self::save_create(); + } + + if ( is_wp_error( $ret ) ) { + // Translators: %1$s = Error message; %2$s = Error code. + LLMS_Admin_Settings::set_error( sprintf( __( 'Error: %1$s [Code: %2$s]', 'lifterlms' ), $ret->get_error_message(), $ret->get_error_code() ) ); + } + + return $ret; + + } + + /** + * Form handler to create a new API key. + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_API_Key|WP_Error + */ + protected static function save_create() { + + $create = LLMS_REST_API()->keys()->create( + array( + 'description' => llms_filter_input( INPUT_POST, 'llms_rest_key_description', FILTER_SANITIZE_STRING ), + 'user_id' => llms_filter_input( INPUT_POST, 'llms_rest_key_user_id', FILTER_SANITIZE_NUMBER_INT ), + 'permissions' => llms_filter_input( INPUT_POST, 'llms_rest_key_permissions', FILTER_SANITIZE_STRING ), + ) + ); + + if ( ! is_wp_error( $create ) ) { + self::$generated_key = $create; + } + + return $create; + + } + + /** + * Form handler to save an API key. + * + * @since 1.0.0-beta.1 + * + * @param int $key_id API Key ID. + * @return LLMS_REST_API_Key|WP_Error + */ + protected static function save_update( $key_id ) { + + $key = LLMS_REST_API()->keys()->get( $key_id ); + if ( ! $key ) { + // Translators: %s = Invalid API Key ID. + return new WP_Error( 'llms_rest_api_key_not_found', sprintf( __( '"%s" is not a valid API Key.', 'lifterlms' ), $key_id ) ); + } + + $update = LLMS_REST_API()->keys()->update( + array( + 'id' => $key_id, + 'description' => llms_filter_input( INPUT_POST, 'llms_rest_key_description', FILTER_SANITIZE_STRING ), + 'user_id' => llms_filter_input( INPUT_POST, 'llms_rest_key_user_id', FILTER_SANITIZE_NUMBER_INT ), + 'permissions' => llms_filter_input( INPUT_POST, 'llms_rest_key_permissions', FILTER_SANITIZE_STRING ), + ) + ); + + return $update; + + } + +} + diff --git a/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-page.php b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-page.php new file mode 100644 index 0000000000..9f715aeadd --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-page.php @@ -0,0 +1,156 @@ +<?php +/** + * Admin Settings Page: REST API + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Admin Settings Page: REST API + * + * @since 1.0.0-beta.1 + */ +class LLMS_Rest_Admin_Settings_Page extends LLMS_Settings_Page { + + /** + * Constructor + * + * @since 1.0.0-beta.1 + */ + public function __construct() { + + require_once 'class-llms-rest-admin-settings-api-keys.php'; + require_once 'class-llms-rest-admin-settings-webhooks.php'; + + $this->id = 'rest-api'; + $this->label = __( 'REST API', 'lifterlms' ); + + // Output Stuff. + add_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); + add_action( 'lifterlms_sections_' . $this->id, array( $this, 'output_sections_nav' ) ); + add_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) ); + + // Maybe Save API Keys. + add_action( 'lifterlms_settings_save_' . $this->id, array( 'LLMS_Rest_Admin_Settings_API_Keys', 'save' ) ); + + // Disable the default page's save button. + add_filter( 'llms_settings_rest-api_has_save_button', '__return_false' ); + + add_filter( 'llms_table_get_table_classes', array( $this, 'get_table_classes' ), 10, 2 ); + add_action( 'lifterlms_admin_field_title-with-html', array( $this, 'output_title_field' ), 10 ); + + } + + /** + * Retrieve the id of the current tab/section + * + * Overrides parent function to set "keys" as the default section instead of the nonexistant "main". + * + * @since 1.0.0-beta.1 + * + * @return string + */ + protected function get_current_section() { + + $current = parent::get_current_section(); + if ( 'main' === $current ) { + $all = array_keys( $this->get_sections() ); + $current = $all ? $all[0] : 'main'; + } + return $current; + + } + + /** + * Get the page sections + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_sections() { + + $sections = array(); + + if ( current_user_can( 'manage_lifterlms_api_keys' ) ) { + $sections['keys'] = __( 'API Keys', 'lifterlms' ); + } + + if ( current_user_can( 'manage_lifterlms_webhooks' ) ) { + $sections['webhooks'] = __( 'Webhooks', 'lifterlms' ); + } + + /** + * Modify the available tabs on the REST API settings screen. + * + * @since 1.0.0-beta.1 + * + * @param array $sections Array of settings page tabs. + */ + return apply_filters( 'llms_rest_api_settings_sections', $sections ); + + } + + /** + * Get settings array + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_settings() { + + $curr_section = $this->get_current_section(); + + $settings = array(); + if ( current_user_can( 'manage_lifterlms_api_keys' ) && 'keys' === $curr_section ) { + $settings = LLMS_Rest_Admin_Settings_API_Keys::get_fields(); + } elseif ( current_user_can( 'manage_lifterlms_webhooks' ) && 'webhooks' === $curr_section ) { + $settings = LLMS_Rest_Admin_Settings_Webhooks::get_fields(); + } + + return apply_filters( 'llms_rest_api_settings_' . $curr_section, $settings ); + + } + + /** + * Add CSS classes to the API Keys Table. + * + * @since 1.0.0-beta.1 + * + * @param string[] $classes Array of css class names. + * @param string $id Table ID. + * @return string[] + */ + public function get_table_classes( $classes, $id ) { + + if ( in_array( $id, array( 'rest-api-keys', 'rest-webhooks' ), true ) ) { + $classes[] = 'text-left'; + } + return $classes; + + } + + /** + * Outputs a custom "title" field with HTML content as the settings section title. + * + * @since 1.0.0-beta.1 + * + * @param array $field Settings field arguments. + * @return void + */ + public function output_title_field( $field ) { + + echo '<p class="llms-label">' . esc_html( $field['title'] ) . ' ' . $field['html'] . '</p>'; + echo '<table class="form-table">'; + + } + +} + +return new LLMS_Rest_Admin_Settings_Page(); diff --git a/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-webhooks.php b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-webhooks.php new file mode 100644 index 0000000000..7bedfa921c --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings-webhooks.php @@ -0,0 +1,200 @@ +<?php +/** + * Admin Settings Page: REST API: Webhooks + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Admin Settings Page: REST API: Webhooks + * + * @since 1.0.0-beta.1 + */ +class LLMS_Rest_Admin_Settings_Webhooks { + + + /** + * Get settings fields for the Keys tab. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public static function get_fields() { + + require_once 'tables/class-llms-rest-table-webhooks.php'; + + $add_hook = '1' === llms_filter_input( INPUT_GET, 'add-webhook', FILTER_SANITIZE_NUMBER_INT ); + $hook_id = llms_filter_input( INPUT_GET, 'edit-webhook', FILTER_SANITIZE_NUMBER_INT ); + + $settings = array(); + + $settings[] = array( + 'class' => 'top', + 'id' => 'rest_hooks_options_start', + 'type' => 'sectionstart', + ); + + $settings[] = array( + 'title' => $hook_id || $add_hook ? __( 'Webhook Details', 'lifterlms' ) : __( 'Webhooks', 'lifterlms' ), + 'type' => 'title-with-html', + 'id' => 'rest_hooks_options_title', + 'html' => $hook_id || $add_hook ? '' : '<a href="' . esc_url( admin_url( 'admin.php?page=llms-settings&tab=rest-api§ion=webhooks&add-webhook=1' ) ) . '" class="llms-button-primary small" type="submit" style="top:-2px;">' . __( 'Add Webhook', 'lifterlms' ) . '</a>', + ); + + if ( $add_hook || $hook_id ) { + + $hook = $add_hook ? false : LLMS_REST_API()->webhooks()->get( $hook_id ); + if ( $add_hook || $hook->exists() ) { + + add_action( 'admin_print_footer_scripts', array( __CLASS__, 'output_scripts' ) ); + + $user_id = $hook ? $hook->get( 'user_id' ) : get_current_user_id(); + + $settings[] = array( + 'title' => __( 'Name', 'lifterlms' ), + 'desc' => '<br>' . __( 'A friendly, human-readable, name used to identify the webhook.', 'lifterlms' ), + 'id' => 'llms_rest_webhook_name', + 'type' => 'text', + 'css' => 'width:480px', + 'value' => $hook ? $hook->get( 'name' ) : '', + ); + + $settings[] = array( + 'title' => __( 'Status', 'lifterlms' ), + 'id' => 'llms_rest_webhook_status', + 'type' => 'select', + 'options' => LLMS_REST_API()->webhooks()->get_statuses(), + 'value' => $hook ? $hook->get( 'status' ) : '', + ); + + $topic = ''; + if ( $hook && 'action' === $hook->get_resource() ) { + $topic = 'action'; + } elseif ( $hook ) { + $topic = $hook->get( 'topic' ); + } + $settings[] = array( + 'title' => __( 'Topic', 'lifterlms' ), + 'id' => 'llms_rest_webhook_topic', + 'type' => 'select', + 'class' => 'llms-select2', + 'options' => LLMS_REST_API()->webhooks()->get_topics(), + 'value' => $topic, + ); + + $settings[] = array( + 'title' => __( 'Action', 'lifterlms' ), + 'id' => 'llms_rest_webhook_action', + 'desc' => '<br>' . __( 'Any registered WordPress, plugin, or theme action hook.', 'lifterlms' ), + 'type' => 'text', + 'value' => $hook ? $hook->get_event() : '', + ); + + $settings[] = array( + 'title' => __( 'Delivery URL', 'lifterlms' ), + 'id' => 'llms_rest_webhook_delivery_url', + 'desc' => '<br>' . __( 'URL where the webhook payload will be delivered.', 'lifterlms' ), + 'type' => 'text', + 'css' => 'width:480px', + 'class' => 'code widefat', + 'value' => $hook ? $hook->get( 'delivery_url' ) : '', + 'custom_attributes' => array( + 'required' => 'required', + ), + ); + + $settings[] = array( + 'title' => __( 'Secret Key', 'lifterlms' ), + 'id' => 'llms_rest_webhook_secret', + 'desc' => '<br>' . __( 'The secret key can be used to verify received payloads originated from this website.', 'lifterlms' ), + 'type' => 'text', + 'css' => 'width:480px', + 'class' => 'code widefat', + 'value' => $hook ? $hook->get( 'secret' ) : '', + ); + + $buttons = '<br><br><button class="llms-button-primary" type="submit" value="llms-rest-save-webhook">' . __( 'Save', 'lifterlms' ) . '</button>'; + if ( $hook ) { + $buttons .= $buttons ? '   ' : '<br><br>'; + $buttons .= '<a class="llms-button-danger" href="' . esc_url( $hook->get_delete_link() ) . '">' . __( 'Delete', 'lifterlms' ) . '</a>'; + } + $buttons .= wp_nonce_field( 'lifterlms-settings', '_wpnonce', true, false ); + + $settings[] = array( + 'type' => 'custom-html', + 'id' => 'llms_rest_webhook_buttons', + 'value' => $buttons, + ); + + $settings[] = array( + 'type' => 'hidden', + 'id' => 'llms_rest_webhook_id', + 'value' => $hook ? $hook->get( 'id' ) : '', + ); + + $settings[] = array( + 'type' => 'hidden', + 'id' => 'llms_rest_webhook_nonce', + 'value' => wp_create_nonce( 'create-update-webhook' ), + ); + + } else { + + $settings[] = array( + 'id' => 'rest_hooks_options_invalid_error', + 'type' => 'custom-html', + 'value' => __( 'Invalid webhook.', 'lifterlms' ), + ); + + } + } else { + + $settings[] = array( + 'id' => 'llms_webhooks_table', + 'table' => new LLMS_REST_Table_Webhooks(), + 'type' => 'table', + ); + + } + + $settings[] = array( + 'id' => 'rest_hooks_options_end', + 'type' => 'sectionend', + ); + + return $settings; + + } + + /** + * Quick and dirty output of JS to power the UI. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public static function output_scripts() { + ?> + <script> + ( function( $ ) { + $( '#llms_rest_webhook_topic' ).on( 'change', function() { + var $action = $( '#llms_rest_webhook_action' ).closest( 'tr' ); + if ( 'action' === $( this ).val() ) { + $action.show(); + } else { + $action.hide(); + } + } ).trigger( 'change' ); + } )( jQuery ); + </script> + <?php + } + +} + diff --git a/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings.php b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings.php new file mode 100644 index 0000000000..376bf45e6f --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/class-llms-rest-admin-settings.php @@ -0,0 +1,51 @@ +<?php +/** + * Manage admin settings pages. + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Manage admin settings pages. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_Admin_Settings { + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function __construct() { + + add_filter( 'lifterlms_get_settings_pages', array( $this, 'add_pages' ) ); + + } + + /** + * Register the REST API settings page with the LifterLMS Core. + * + * @since 1.0.0-beta.1 + * + * @param array $pages Array of settings page classes. + * @return array + */ + public function add_pages( $pages ) { + + $pages[] = include 'class-llms-rest-admin-settings-page.php'; + + return $pages; + + } + +} + +return new LLMS_REST_Admin_Settings(); diff --git a/libraries/lifterlms-rest/includes/admin/index.php b/libraries/lifterlms-rest/includes/admin/index.php new file mode 100644 index 0000000000..9c65c1efa6 --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/index.php @@ -0,0 +1 @@ +<?php // shhh. diff --git a/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-api-keys.php b/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-api-keys.php new file mode 100644 index 0000000000..dbf21f4a54 --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-api-keys.php @@ -0,0 +1,158 @@ +<?php +/** + * API Keys Admin Table. + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Table_API_Keys class.. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_Table_API_Keys extends LLMS_Admin_Table { + + /** + * Unique ID for the Table + * + * @var string + */ + protected $id = 'rest-api-keys'; + + /** + * If true will be a table with a larger font size + * + * @var bool + */ + protected $is_large = true; + + /** + * Retrieve information for a the api key title/description <td> + * + * @since 1.0.0-beta.1 + * + * @param LLMS_REST_API_Key $api_key API Key object. + * @return string + */ + protected function get_description_cell( $api_key ) { + + $html = esc_html( $api_key->get( 'description' ) ); + $edit_link = esc_url( $api_key->get_edit_link() ); + $html = '<a href="' . $edit_link . '">' . $html . '</a>'; + $html .= '<div class="llms-rest-actions">'; + $html .= '<small class="llms-action-icon">ID: ' . $api_key->get( 'id' ) . '</small> | '; + $html .= '<small><a class="llms-action-icon" href="' . $edit_link . '">' . __( 'View/Edit', 'lifterlms' ) . '</a></small> | '; + $html .= '<small><a class="llms-action-icon danger" href="' . esc_url( $api_key->get_delete_link() ) . '">' . __( 'Revoke', 'lifterlms' ) . '</a></small>'; + $html .= '</div>'; + + return $html; + + } + + /** + * Retrieve data for the columns + * + * @since 1.0.0-beta.1 + * + * @param string $key the column id / key. + * @param LLMS_REST_API_Key $api_key API key object. + * @return mixed + */ + public function get_data( $key, $api_key ) { + + switch ( $key ) { + + case 'description': + $value = $this->get_description_cell( $api_key ); + break; + + case 'last_access': + $value = $api_key->get_last_access_date(); + break; + + case 'truncated_key': + $value = '<code>…' . $api_key->get( $key ) . '</code>'; + break; + + case 'user_id': + $user = get_user_by( 'id', $api_key->get( $key ) ); + if ( ! $user ) { + $value = ''; + } elseif ( current_user_can( 'edit_user', $user->ID ) ) { + $value = '<a href="' . esc_url( get_edit_user_link( $user->ID ) ) . '">' . esc_html( $user->display_name ) . '</a>'; + } else { + $value = esc_html( $user->display_name ); + } + + break; + + default: + $value = $api_key->get( $key ); + + } + + return $this->filter_get_data( $value, $key, $api_key ); + + } + + /** + * Execute a query to retrieve results from the table + * + * @since 1.0.0-beta.1 + * + * @param array $args Array of query args. + * + * @return void + */ + public function get_results( $args = array() ) { + + global $wpdb; + + $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}lifterlms_api_keys", ARRAY_A ); + + $tbody_data = array(); + foreach ( $rows as $data ) { + $key = new LLMS_REST_API_Key( $data['id'], false ); + $tbody_data[] = $key->setup( $data ); + } + + $this->tbody_data = $tbody_data; + + } + + /** + * Define the structure of arguments used to pass to the get_results method + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function set_args() { + return array(); + } + + /** + * Define the structure of the table + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function set_columns() { + + return array( + 'description' => __( 'Description', 'lifterlms' ), + 'truncated_key' => __( 'Consumer key', 'lifterlms' ), + 'user_id' => __( 'User', 'lifterlms' ), + 'permissions' => __( 'Permissions', 'lifterlms' ), + 'last_access' => __( 'Last Access', 'lifterlms' ), + ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-webhooks.php b/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-webhooks.php new file mode 100644 index 0000000000..0c1e265fd2 --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/tables/class-llms-rest-table-webhooks.php @@ -0,0 +1,144 @@ +<?php +/** + * API Keys Admin Table. + * + * @package LifterLMS_REST/Admin/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Table_Webhooks class.. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Output translated status instead of the database value; trim the delivery URL to 40 characters. + */ +class LLMS_REST_Table_Webhooks extends LLMS_Admin_Table { + + /** + * Unique ID for the Table + * + * @var string + */ + protected $id = 'rest-webhooks'; + + /** + * If true will be a table with a larger font size + * + * @var bool + */ + protected $is_large = true; + + /** + * Retrieve information for a the webhook title/description <td> + * + * @since 1.0.0-beta.1 + * + * @param LLMS_REST_API_Key $webhook API Key object. + * @return string + */ + protected function get_name_cell( $webhook ) { + + $html = esc_html( $webhook->get( 'name' ) ); + $edit_link = esc_url( $webhook->get_edit_link() ); + $html = '<a href="' . $edit_link . '">' . $html . '</a>'; + $html .= '<div class="llms-rest-actions">'; + $html .= '<small class="llms-action-icon">ID: ' . $webhook->get( 'id' ) . '</small> | '; + $html .= '<small><a class="llms-action-icon" href="' . $edit_link . '">' . __( 'View/Edit', 'lifterlms' ) . '</a></small> | '; + $html .= '<small><a class="llms-action-icon danger" href="' . esc_url( $webhook->get_delete_link() ) . '">' . __( 'Delete', 'lifterlms' ) . '</a></small>'; + $html .= '</div>'; + + return $html; + + } + + /** + * Retrieve data for the columns + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Output translated status instead of the database value; trim the delivery URL to 40 characters. + * + * @param string $key the column id / key. + * @param LLMS_REST_API_Key $webhook API key object. + * @return mixed + */ + public function get_data( $key, $webhook ) { + + switch ( $key ) { + + case 'name': + $value = $this->get_name_cell( $webhook ); + break; + + case 'status': + $statuses = LLMS_REST_API()->webhooks()->get_statuses(); + $value = $webhook->get( $key ); + $value = isset( $statuses[ $value ] ) ? $statuses[ $value ] : $value; + break; + + case 'delivery_url': + $value = llms_trim_string( $webhook->get( $key ), 40 ); + break; + + default: + $value = $webhook->get( $key ); + + } + + return $this->filter_get_data( $value, $key, $webhook ); + + } + + /** + * Execute a query to retrieve results from the table + * + * @since 1.0.0-beta.1 + * + * @param array $args Array of query args. + * + * @return void + */ + public function get_results( $args = array() ) { + + $args = wp_parse_args( $args, $this->set_args() ); + + $query = new LLMS_REST_Webhooks_Query( $args ); + $this->tbody_data = $query->get_webhooks(); + + } + + /** + * Define the structure of arguments used to pass to the get_results method + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function set_args() { + return array( + 'per_page' => 999, + ); + } + + /** + * Define the structure of the table + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function set_columns() { + + return array( + 'name' => __( 'Name', 'lifterlms' ), + 'status' => __( 'Status', 'lifterlms' ), + 'topic' => __( 'Topic', 'lifterlms' ), + 'delivery_url' => __( 'Delivery URL', 'lifterlms' ), + ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/admin/tables/index.php b/libraries/lifterlms-rest/includes/admin/tables/index.php new file mode 100644 index 0000000000..9c65c1efa6 --- /dev/null +++ b/libraries/lifterlms-rest/includes/admin/tables/index.php @@ -0,0 +1 @@ +<?php // shhh. diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-api-keys-query.php b/libraries/lifterlms-rest/includes/class-llms-rest-api-keys-query.php new file mode 100644 index 0000000000..58112e8552 --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-api-keys-query.php @@ -0,0 +1,204 @@ +<?php +/** + * Perform db queries for API Keys + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.16 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_API_Keys_Query class + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_API_Keys_Query extends LLMS_Database_Query { + + /** + * Identify the Query + * + * @var string + */ + protected $id = 'rest_api_key'; + + /** + * Retrieve default arguments for a query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `this->get_filter( 'default_args' )` in favor of `'llms_rest_api_key_query_default_args'`. + * + * @return array + */ + protected function get_default_args() { + + $args = array( + 'include' => array(), + 'exclude' => array(), + 'per_page' => 10, + 'permissions' => '', + 'user' => array(), + 'user_not_in' => array(), + ); + + $args = wp_parse_args( $args, parent::get_default_args() ); + + if ( $args['suppress_filters'] ) { + return $args; + } + + /** + * Filters the api keys query default args + * + * @since 1.0.0-beta.1 + * + * @param array $args Array of default arguments to set up the query with. + * @param LLMS_REST_API_Keys_Query $api_keys_query Instance of LLMS_REST_API_Keys_Query. + */ + return apply_filters( 'llms_rest_api_key_query_default_args', $args, $this ); + + } + + /** + * Retrieve an array of LLMS_REST_API_Keys for the given result set returned by the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `this->get_filter( 'get_keys' )` in favor of `'llms_rest_api_key_query_get_keys'`. + * + * @return array + */ + public function get_keys() { + + $keys = array(); + $results = $this->get_results(); + + if ( $results ) { + + foreach ( $results as $result ) { + $keys[] = LLMS_REST_API()->keys()->get( $result->id, true ); + } + } + + if ( $this->get( 'suppress_filters' ) ) { + return $keys; + } + + /** + * Filters the list of API Keys + * + * @since 1.0.0-beta.1 + * + * @param LLMS_REST_API_Key[] $keys Array of LLMS_REST_API_Key instances. + * @param LLMS_REST_API_Keys_Query $api_keys_query Instance of LLMS_REST_API_Keys_Query. + */ + return apply_filters( 'llms_rest_api_key_query_get_keys', $keys, $this ); + + } + + /** + * Parses argument data + * + * @since 1.0.0-beta.1 + * + * @return void + */ + protected function parse_args() { + + // Sanitize post & user ids. + foreach ( array( 'include', 'exclude', 'user', 'user_not_in' ) as $key ) { + $this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] ); + } + + // Validate permissions. + $permissions = $this->get( 'permissions' ); + if ( $permissions && ! in_array( $permissions, array_keys( LLMS_REST_API()->keys()->get_permissions() ), true ) ) { + $this->arguments['permissions'] = ''; + } + + } + + /** + * Prepare the SQL for the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Use `$this->sql_select_columns({columns})` to determine the columns to select. + * + * @return string + */ + protected function preprare_query() { + + global $wpdb; + + return "SELECT {$this->sql_select_columns( 'id' )} + FROM {$wpdb->prefix}lifterlms_api_keys + {$this->sql_where()} + {$this->sql_orderby()} + {$this->sql_limit()};"; + + } + + /** + * SQL "where" clause for the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `$this->get_filter('where')` in favor of `'llms_rest_api_key_query_where'`. + * + * @return string + */ + protected function sql_where() { + + global $wpdb; + + $sql = 'WHERE 1'; + + // "IN" clauses for id fields. + $ids_include = array( + 'include' => 'id', + 'user' => 'user_id', + ); + foreach ( $ids_include as $query_key => $db_key ) { + $ids = $this->get( $query_key ); + if ( $ids ) { + $prepared = implode( ',', $ids ); + $sql .= " AND {$db_key} IN ({$prepared})"; + } + } + + // "NOT IN" clauses for id fields. + $ids_exclude = array( + 'exclude' => 'id', + 'user_not_in' => 'user_id', + ); + foreach ( $ids_exclude as $query_key => $db_key ) { + $ids = $this->get( $query_key ); + if ( $ids ) { + $prepared = implode( ',', $ids ); + $sql .= " AND {$db_key} NOT IN ({$prepared})"; + } + } + + // Permission match. + $permissions = $this->get( 'permissions' ); + if ( $permissions ) { + $sql .= $wpdb->prepare( ' AND permissions = %s', $permissions ); + } + + if ( $this->get( 'suppress_filters' ) ) { + return $sql; + } + + /** + * Filters the query WHERE clause + * + * @since 1.0.0-beta.1 + * + * @param string $sql The WHERE clause of the query. + * @param LLMS_REST_API_Keys_Query $apy_keys__query Instance of LLMS_REST_API_Keys_Query. + */ + return apply_filters( 'llms_rest_api_key_query_where', $sql, $this ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-api-keys.php b/libraries/lifterlms-rest/includes/class-llms-rest-api-keys.php new file mode 100644 index 0000000000..bce0a545fb --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-api-keys.php @@ -0,0 +1,194 @@ +<?php +/** + * CRUD API Keys. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_API_Keys class. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_API_Keys extends LLMS_REST_Database_Resource { + + use LLMS_REST_Trait_Singleton; + + /** + * Resource Name/ID key. + * + * EG: key. + * + * @var string + */ + protected $id = 'key'; + + /** + * Resource Model classname. + * + * EG: LLMS_REST_API_Key. + * + * @var string + */ + protected $model = 'LLMS_REST_API_Key'; + + /** + * Default column values (for creating). + * + * @var array + */ + protected $default_column_values = array( + 'permissions' => 'read', + ); + + /** + * Array of read only column names. + * + * @var array + */ + protected $read_only_columns = array( + 'id', + 'consumer_key', + 'consumer_secret', + 'truncated_key', + ); + + /** + * Array of required columns (for creating). + * + * @var array + */ + protected $required_columns = array( + 'description', + 'user_id', + 'permissions', + ); + + /** + * Create a new API Key + * + * @since 1.0.0-beta.1 + * + * @param array $data { + * Associative array of data to set to a key's properties. + * + * @type string $description (Required) A friendly name for the key. + * @type int $user_id WP_User (Required) ID of the key's owner. + * @type string $permissions (Required) Permission string for the key. Accepts `read`, `write`, or `read_write`. + * } + * @return WP_Error|LLMS_REST_API_Key + */ + public function create( $data ) { + + $data = $this->create_prepare( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + $api_key = new LLMS_REST_API_Key(); + + $key = 'ck_' . llms_rest_random_hash(); + $secret = 'cs_' . llms_rest_random_hash(); + + $data['consumer_key'] = llms_rest_api_hash( $key ); + $data['consumer_secret'] = $secret; + $data['truncated_key'] = substr( $key, -7 ); + + // Set and save. + $api_key->setup( $data )->save(); + + // Return the unhashed key on creation to be displayed once and never stored. + $api_key->set( 'consumer_key_one_time', $key ); + + return $api_key; + + } + + /** + * Retrieve the base admin url for managing API keys. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_admin_url() { + return add_query_arg( + array( + 'page' => 'llms-settings', + 'tab' => 'rest-api', + 'section' => 'keys', + ), + admin_url( 'admin.php' ) + ); + } + + /** + * Retrieve the translated resource name. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + protected function get_i18n_name() { + return __( 'API Key', 'lifterlms' ); + } + + /** + * Retrieve an array of options for API Key Permissions. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_permissions() { + return array( + 'read' => __( 'Read', 'lifterlms' ), + 'write' => __( 'Write', 'lifterlms' ), + 'read_write' => __( 'Read / Write', 'lifterlms' ), + ); + } + + /** + * Validate data supplied for creating/updating a key. + * + * @since 1.0.0-beta.1 + * + * @param array $data { + * Associative array of data to set to a key's properties. + * + * @type string $description A friendly name for the key. + * @type int $user_id WP_User ID of the key's owner. + * @type string $permissions Permission string for the key. Accepts `read`, `write`, or `read_write`. + * } + * @return WP_Error|true When data is invalid will return a WP_Error with information about the invalid properties, + * otherwise `true` denoting data is valid. + */ + protected function is_data_valid( $data ) { + + // First conditions prevents '', '0', 0, etc... & second prevents invalid / non existant user ids. + if ( ( isset( $data['user_id'] ) && empty( $data['user_id'] ) ) || ( ! empty( $data['user_id'] ) && ! get_user_by( 'id', $data['user_id'] ) ) ) { + // Translators: %s = Invalid user id. + return new WP_Error( 'llms_rest_key_invalid_user_id', sprintf( __( '"%s" is not a valid user ID.', 'lifterlms' ), $data['user_id'] ) ); + } + + // Prevent blank/empty descriptions. + if ( isset( $data['description'] ) && empty( $data['description'] ) ) { + return new WP_Error( 'llms_rest_key_invalid_description', __( 'An API Description is required.', 'lifterlms' ) ); + } + + // Validate Permissions. + if ( ! empty( $data['permissions'] ) && ! in_array( $data['permissions'], array_keys( $this->get_permissions() ), true ) ) { + // Translators: %s = Invalid permission string. + return new WP_Error( 'llms_rest_key_invalid_permissions', sprintf( __( '"%s" is not a valid permission.', 'lifterlms' ), $data['permissions'] ) ); + } + + return true; + + } + +} diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-authentication.php b/libraries/lifterlms-rest/includes/class-llms-rest-authentication.php new file mode 100644 index 0000000000..3a9f19ff48 --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-authentication.php @@ -0,0 +1,307 @@ +<?php +/** + * REST API Authentication. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.12 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Authentication. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.5 is_rest_request() accesses uses `filter_var` instead of `llms_filter_input()`. + * Load all includes to accommodate plugins and themes that call `determine_current_user` early. + * @since 1.0.0-beta.12 Call `llms_rest_authorization_required_error()` instructing to not check if the current user is logged in + * to avoid infinite loops. + */ +class LLMS_REST_Authentication { + + /** + * Authenticated API key for the current request. + * + * @var LLMS_REST_API_Key + */ + protected $api_key = null; + + /** + * Authentication error object. + * + * @var WP_Error + */ + protected $error = null; + + /** + * Constructor + * + * @since 1.0.0-beta.1 + */ + public function __construct() { + + /** + * Disable LifterLMS REST API Key authentication in favor of a custom authentication solution. + * + * @param bool $use_auth When true, LifterLMS Basic (or header) authorization will be used. + */ + $use_auth = apply_filters( 'llms_rest_use_authentication', true ); + if ( $use_auth ) { + + add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 ); + add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 ); + add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); + add_filter( 'rest_pre_dispatch', array( $this, 'check_permissions' ), 10, 3 ); + + } + + } + + /** + * Authenticate an API Request + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.5 Load all includes to accommodate plugins and themes that call `determine_current_user` early. + * @since 1.0.0-beta.12 Call `llms_rest_authorization_required_error()` instructing to not check if the current user is logged in + * to avoid infinite loops. + * + * @link https://developer.wordpress.org/reference/hooks/determine_current_user/ + * + * @param int|false $user_id WP_User ID of an already authenticated user or false. + * @return int|false + */ + public function authenticate( $user_id ) { + + // Load includes in case a plugin has triggered authentication early. + LLMS_REST_API()->includes(); + + // 1. If we already have a user, use that user. + // 2. Only authenticate via ssl. + // 3. Only authenticate to our end points. + if ( ! empty( $user_id ) || ! is_ssl() || ! $this->is_rest_request() ) { + return $user_id; + } + + $creds = $this->locate_credentials(); + if ( ! $creds ) { + return false; + } + + $key = $this->find_key( $creds['key'] ); + if ( ! $key ) { + return false; + } + + if ( ! hash_equals( $key->get( 'consumer_secret' ), $creds['secret'] ) ) { + $this->set_error( llms_rest_authorization_required_error( '', false ) ); + return false; + } + + $this->api_key = $key; + + $user_id = $key->get( 'user_id' ); + do_action( 'llms_rest_basic_auth_success', $user_id ); + + return $user_id; + + } + + /** + * Check for authentication error. + * + * @since 1.0.0-beta.1 + * + * @link https://developer.wordpress.org/reference/hooks/rest_authentication_errors/ + * + * @param WP_Error|null|bool $error Existing error data. + * @return WP_Error|null|bool + */ + public function check_authentication_error( $error ) { + + // Pass through existing errors. + if ( ! empty( $error ) ) { + return $error; + } + + return $this->get_error(); + + } + + /** + * Check if the API Key can perform the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Call `llms_rest_authorization_required_error()` instructing to not check if the current user is logged in + * to avoid infinite loops. + * + * @param mixed $result Response to replace the requested version with. + * @param WP_REST_Server $server Server instance. + * @param WP_REST_Request $request Request used to generate the response. + * @return mixed + */ + public function check_permissions( $result, $server, $request ) { + + if ( $this->api_key ) { + + $allowed = $this->api_key->has_permission( $request->get_method() ); + if ( ! $allowed ) { + return llms_rest_authorization_required_error( '', false ); + } + + // Update the API key's last access time. + $this->api_key->set( 'last_access', current_time( 'mysql' ) )->save(); + + } + + return $result; + } + + /** + * Find a key via unhashed consumer key + * + * @since 1.0.0-beta.1 + * + * @param string $consumer_key An unhashed consumer key. + * @return LLMS_REST_API_Key|false + */ + protected function find_key( $consumer_key ) { + + global $wpdb; + + $consumer_key = llms_rest_api_hash( $consumer_key ); + + $key_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}lifterlms_api_keys WHERE consumer_key = %s", $consumer_key ) ); + + if ( $key_id ) { + return LLMS_REST_API()->keys()->get( $key_id ); + } + + return false; + + } + + /** + * Locate credentials in the $_SERVER superglobal. + * + * @since 1.0.0-beta.1 + * + * @param string $key_var Variable name for the consumer key. + * @param string $secret_var Variable name for the consumer secret. + * @return array|false + */ + private function get_credentials( $key_var, $secret_var ) { + + // Use `filter_var()` instead of `llms_filter_input()` due to PHP bug with `filter_input()`: https://bugs.php.net/bug.php?id=49184. + $key = isset( $_SERVER[ $key_var ] ) ? filter_var( wp_unslash( $_SERVER[ $key_var ] ), FILTER_SANITIZE_STRING ) : null; + $secret = isset( $_SERVER[ $secret_var ] ) ? filter_var( wp_unslash( $_SERVER[ $secret_var ] ), FILTER_SANITIZE_STRING ) : null; + + if ( ! $key || ! $secret ) { + return false; + } + + return compact( 'key', 'secret' ); + + } + + /** + * Retrieve the auth error object. + * + * @since 1.0.0-beta.1 + * + * @return WP_Error|null + */ + protected function get_error() { + return $this->error; + } + + /** + * Determine if the request is a request to a LifterLMS REST API endpoint. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.5 Access `$_SERVER['REQUEST_URI']` via `filter_var` instead of `llms_filter_input()`, see https://bugs.php.net/bug.php?id=49184. + * + * @return bool + */ + protected function is_rest_request() { + + $request = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ) : null; + if ( empty( $request ) ) { + return false; + } + if ( empty( $request ) ) { + return false; + } + + $request = esc_url_raw( wp_unslash( $request ) ); + $prefix = trailingslashit( rest_get_url_prefix() ); + + $core = ( false !== strpos( $request, $prefix . 'llms/' ) ); + + // Allow 3rd parties to use core auth. + $external = ( false !== strpos( $request, $prefix . 'llms-' ) ); + + return apply_filters( 'llms_is_rest_request', $core || $external, $request ); + + } + + /** + * Get api credentials from headers and then basic auth. + * + * @since 1.0.0-beta.1 + * + * @return array|false + */ + protected function locate_credentials() { + + // Attempt to get creds from headers. + $creds = $this->get_credentials( 'HTTP_X_LLMS_CONSUMER_KEY', 'HTTP_X_LLMS_CONSUMER_SECRET' ); + if ( $creds ) { + return $creds; + } + + // Attempt to get creds from basic auth. + $creds = $this->get_credentials( 'PHP_AUTH_USER', 'PHP_AUTH_PW' ); + if ( $creds ) { + return $creds; + } + + return false; + + } + + /** + * Return a WWW-Authenticate header error message when incorrect creds are supplied + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate + * + * @param WP_REST_Response $response Current response being served. + * @return WP_REST_Response + */ + public function send_unauthorized_headers( $response ) { + + if ( is_wp_error( $this->get_error() ) ) { + $auth_message = __( 'LifterLMS REST API', 'lifterlms' ); + $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); + } + + return $response; + + } + + /** + * Set authentication error object. + * + * @since 1.0.0-beta.1 + * + * @param WP_Error|null $err Error object or null to clear an error. + * @return void + */ + protected function set_error( $err ) { + $this->error = $err; + } + +} + +return new LLMS_REST_Authentication(); diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-capabilities.php b/libraries/lifterlms-rest/includes/class-llms-rest-capabilities.php new file mode 100644 index 0000000000..169ce24a4d --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-capabilities.php @@ -0,0 +1,52 @@ +<?php +/** + * Manage custom user capabilities. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Capabilities class. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_Capabilities { + + /** + * Static Constructor. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public static function init() { + + add_filter( 'llms_get_administrator_core_caps', array( __CLASS__, 'add' ) ); + add_filter( 'llms_get_lms_manager_core_caps', array( __CLASS__, 'add' ) ); + + } + + /** + * Add REST-specific capabilities to LifterLMS core cap lists. + * + * @since 1.0.0-beta.1 + * + * @see LLMS_Roles::get_core_caps() + * + * @param array $caps Assoc. array of existing caps, array key is the capability and the value is a bool (true = has cap). + * @return array + */ + public static function add( $caps ) { + $caps['manage_lifterlms_api_keys'] = true; + $caps['manage_lifterlms_webhooks'] = true; + return $caps; + } + +} + +return LLMS_REST_Capabilities::init(); diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-install.php b/libraries/lifterlms-rest/includes/class-llms-rest-install.php new file mode 100644 index 0000000000..08627dbfb4 --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-install.php @@ -0,0 +1,137 @@ +<?php +/** + * Plugin installation scripts. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.17 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Plugin installation scripts. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_Install { + + /** + * Initialize the install class. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public static function init() { + add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); + add_filter( 'llms_install_get_schema', array( __CLASS__, 'get_schema' ), 20, 2 ); + } + + /** + * Checks the current LLMS version and runs installer if required + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public static function check_version() { + if ( ! defined( 'IFRAME_REQUEST' ) && get_option( 'llms_rest_version' ) !== LLMS_REST_API()->version ) { + self::install(); + do_action( 'llms_rest_updated' ); + } + + } + + /** + * Adds REST API Keys table to the LifterLMS DB Table Schema + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Remove unused 'pending_delivery' column. + * + * @see LLMS_Install::get_schema() + * + * @param string $schema String of DB table creation statements. + * @param string $collate Collation string. + * @return string + */ + public static function get_schema( $schema, $collate ) { + + global $wpdb; + + $schema .= " +CREATE TABLE `{$wpdb->prefix}lifterlms_api_keys` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `description` varchar(200) DEFAULT NULL, + `permissions` varchar(10) NOT NULL, + `consumer_key` char(64) NOT NULL, + `consumer_secret` char(43) NOT NULL, + `truncated_key` char(7) NOT NULL, + `last_access` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `consumer_key` (`consumer_key`), + KEY `consumer_secret` (`consumer_secret`) +) $collate; +CREATE TABLE `{$wpdb->prefix}lifterlms_webhooks` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `status` varchar(20) NOT NULL, + `name` text NOT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `delivery_url` text NOT NULL, + `secret` text NOT NULL, + `topic` varchar(255) NOT NULL, + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `failure_count` smallint(3) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`) +) $collate; + + "; + + return $schema; + + } + + /** + * Core install function + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public static function install() { + + if ( ! is_blog_installed() ) { + return; + } + + do_action( 'llms_rest_before_install' ); + + LLMS_Roles::install(); + LLMS_Install::create_tables(); + self::update_version(); + + do_action( 'llms_rest_after_install' ); + + } + + + /** + * Update the LifterLMS rest version record to the latest version + * + * @since 1.0.0-beta.1 + * + * @param string $version version number. + * @return void + */ + public static function update_version( $version = null ) { + delete_option( 'llms_rest_version' ); + add_option( 'llms_rest_version', is_null( $version ) ? LLMS_REST_API()->version : $version ); + } + +} + +LLMS_REST_Install::init(); diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-webhooks-query.php b/libraries/lifterlms-rest/includes/class-llms-rest-webhooks-query.php new file mode 100644 index 0000000000..7f0a602f72 --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-webhooks-query.php @@ -0,0 +1,200 @@ +<?php +/** + * Perform db queries for Webhooks + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.16 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Webhooks_Query class. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_Webhooks_Query extends LLMS_Database_Query { + + /** + * Identify the Query + * + * @var string + */ + protected $id = 'rest_webhook'; + + /** + * Retrieve default arguments for a query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `this->get_filter( 'default_args' )` in favor of `'llms_rest_webhook_query_default_args'`. + * + * @return array + */ + protected function get_default_args() { + + $args = array( + 'include' => array(), + 'exclude' => array(), + 'status' => '', + 'per_page' => 10, + ); + + $args = wp_parse_args( $args, parent::get_default_args() ); + + if ( $args['suppress_filters'] ) { + return $args; + } + + /** + * Filters the webhooks query default args + * + * @since 1.0.0-beta.1 + * + * @param array $args Array of default arguments to set up the query with. + * @param LLMS_REST_Webhooks_Query $webhooks_query Instance of LLMS_REST_Webhooks_Query. + */ + return apply_filters( 'llms_rest_webhook_query_default_args', $args, $this ); + + } + + /** + * Retrieve an array of LLMS_REST_Webhook objects for the given result set returned by the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `this->get_filter( 'get_webhooks' )` in favor of `'llms_rest_webhook_query_get_webhooks'`. + * + * @return array + */ + public function get_webhooks() { + + $hooks = array(); + $results = $this->get_results(); + + if ( $results ) { + + foreach ( $results as $result ) { + $hooks[] = LLMS_REST_API()->webhooks()->get( $result->id, true ); + } + } + + if ( $this->get( 'suppress_filters' ) ) { + return $hooks; + } + + /** + * Filters the list of webhooks + * + * @since 1.0.0-beta.1 + * + * @param LLMS_REST_Webhook[] $webhooks Array of LLMS_REST_Webhook instances. + * @param LLMS_REST_Webhooks_Query $webhooks_query Instance of LLMS_REST_Webhooks_Query. + */ + return apply_filters( 'llms_rest_webhook_query_get_webhooks', $hooks, $this ); + + } + + /** + * Parses argument data + * + * @since 1.0.0-beta.1 + * + * @return void + */ + protected function parse_args() { + + // Sanitize post & user ids. + foreach ( array( 'include', 'exclude' ) as $key ) { + $this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] ); + } + + // Validate status. + $status = $this->get( 'status' ); + if ( $status && ! in_array( $status, array_keys( LLMS_REST_API()->webhooks()->get_statuses() ), true ) ) { + $this->arguments['status'] = ''; + } + + } + + /** + * Prepare the SQL for the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Use `$this->sql_select_columns({columns})` to determine the columns to select. + * + * @return string + */ + protected function preprare_query() { + + global $wpdb; + + return "SELECT {$this->sql_select_columns( 'id' )} + FROM {$wpdb->prefix}lifterlms_webhooks + {$this->sql_where()} + {$this->sql_orderby()} + {$this->sql_limit()};"; + + } + + /** + * SQL "where" clause for the query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 Drop usage of `$this->get_filter('where')` in favor of `'llms_rest_webhook_query_where'`. + * + * @return string + */ + protected function sql_where() { + + global $wpdb; + + $sql = 'WHERE 1'; + + // "IN" clauses for id fields. + $ids_include = array( + 'include' => 'id', + ); + foreach ( $ids_include as $query_key => $db_key ) { + $ids = $this->get( $query_key ); + if ( $ids ) { + $prepared = implode( ',', $ids ); + $sql .= " AND {$db_key} IN ({$prepared})"; + } + } + + // "NOT IN" clauses for id fields. + $ids_exclude = array( + 'exclude' => 'id', + ); + foreach ( $ids_exclude as $query_key => $db_key ) { + $ids = $this->get( $query_key ); + if ( $ids ) { + $prepared = implode( ',', $ids ); + $sql .= " AND {$db_key} NOT IN ({$prepared})"; + } + } + + // Status match. + $status = $this->get( 'status' ); + if ( $status ) { + $sql .= $wpdb->prepare( ' AND status = %s', $status ); + } + + if ( $this->get( 'suppress_filters' ) ) { + return $sql; + } + + /** + * Filters the query WHERE clause + * + * @since 1.0.0-beta.1 + * + * @param string $sql The WHERE clause of the query. + * @param LLMS_REST_Webhooks_Query $webhooks_query Instance of LLMS_REST_Webhooks_Query. + */ + return apply_filters( 'llms_rest_webhook_query_where', $sql, $this ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/class-llms-rest-webhooks.php b/libraries/lifterlms-rest/includes/class-llms-rest-webhooks.php new file mode 100644 index 0000000000..740b84ceae --- /dev/null +++ b/libraries/lifterlms-rest/includes/class-llms-rest-webhooks.php @@ -0,0 +1,579 @@ +<?php +/** + * CRUD Webhooks + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.18 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Webhooks class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Fix formatting error on the default webhook name string. + * @since 1.0.0-beta.6 "access plan" not "access_plan" for human reading. + * @since 1.0.0-beta.11 `'save_post_*'` hooks number of arguments reduced to two. + */ +class LLMS_REST_Webhooks extends LLMS_REST_Database_Resource { + + use LLMS_REST_Trait_Singleton; + + /** + * Resource Name/ID key. + * + * @var string + */ + protected $id = 'webhook'; + + /** + * Resource Model classname. + * + * @var string + */ + protected $model = 'LLMS_REST_Webhook'; + + /** + * Default column values (for creating). + * + * @var array + */ + protected $default_column_values = array(); + + /** + * Array of read only column names. + * + * @var array + */ + protected $read_only_columns = array( + 'id', + ); + + /** + * Array of required columns (for creating). + * + * @var array + */ + protected $required_columns = array( + 'topic', + 'delivery_url', + ); + + /** + * Create a new API Key + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column. + * + * @param array $data Associative array of data to set to a key's properties. + * @return WP_Error|LLMS_REST_Webhook + */ + public function create( $data ) { + + $data = $this->create_prepare( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + // Can't set this property during creation. + unset( $data['failure_count'] ); + + return $this->save( new $this->model(), $data ); + + } + + /** + * Retrieve the base admin url for managing API keys. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_admin_url() { + return add_query_arg( + array( + 'page' => 'llms-settings', + 'tab' => 'rest-api', + 'section' => 'webhooks', + ), + admin_url( 'admin.php' ) + ); + } + + /** + * Get default column values. + * + * Overrides parent to dynamically set the class variable since several defaults are generated through functions. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Fix formatting error. + * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column. + * + * @return array + */ + public function get_default_column_values() { + + $this->default_column_values = array( + 'secret' => wp_generate_password( 50, true, true ), + 'status' => 'disabled', + 'failure_count' => 0, + 'user_id' => get_current_user_id(), + 'name' => sprintf( + // Translators: %s = created date. + __( 'Webhook created on %s', 'lifterlms' ), + // Translators: Date format. + strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'lifterlms' ) ) // phpcs:disable WordPress.WP.I18n.UnorderedPlaceholdersText + ), + ); + + return parent::get_default_column_values(); + + } + + /** + * Retrieve the translated resource name. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + protected function get_i18n_name() { + return __( 'Webhook', 'lifterlms' ); + } + + /** + * Retrieves a list of webhook statuses. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_statuses() { + + /** + * Filter the available webhook statuses. + * + * @since 1.0.0-beta.1 + * + * @param array $statuses Array of statuses. + */ + return apply_filters( + 'llms_rest_webhook_statuses', + array( + 'active' => __( 'Active', 'lifterlms' ), + 'paused' => __( 'Paused', 'lifterlms' ), + 'disabled' => __( 'Disabled', 'lifterlms' ), + ) + ); + + } + + /** + * Retrieves a list of webhook topics. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.6 Fix translated access plans typo. + * @since 1.0.0-beta.18 Remove access_plan.restored topic - access plan post type doesn't support trashing. + * + * @return array + */ + public function get_topics() { + + /** + * Filter the available webhook topics. + * + * @since 1.0.0-beta.1 + * + * @param array $topics Array of topics. + */ + return apply_filters( + 'llms_rest_webhook_topics', + array( + 'course.created' => __( 'Course created', 'lifterlms' ), + 'course.updated' => __( 'Course updated', 'lifterlms' ), + 'course.deleted' => __( 'Course deleted', 'lifterlms' ), + 'course.restored' => __( 'Course restored', 'lifterlms' ), + 'section.created' => __( 'Section created', 'lifterlms' ), + 'section.updated' => __( 'Section updated', 'lifterlms' ), + 'section.deleted' => __( 'Section deleted', 'lifterlms' ), + 'lesson.created' => __( 'Lesson created', 'lifterlms' ), + 'lesson.updated' => __( 'Lesson updated', 'lifterlms' ), + 'lesson.deleted' => __( 'Lesson deleted', 'lifterlms' ), + 'lesson.restored' => __( 'Lesson restored', 'lifterlms' ), + 'membership.created' => __( 'Membership created', 'lifterlms' ), + 'membership.updated' => __( 'Membership updated', 'lifterlms' ), + 'membership.deleted' => __( 'Membership deleted', 'lifterlms' ), + 'membership.restored' => __( 'Membership restored', 'lifterlms' ), + 'access_plan.created' => __( 'Access Plan created', 'lifterlms' ), + 'access_plan.updated' => __( 'Access Plan updated', 'lifterlms' ), + 'access_plan.deleted' => __( 'Access Plan deleted', 'lifterlms' ), + 'order.created' => __( 'Order created', 'lifterlms' ), + 'order.updated' => __( 'Order updated', 'lifterlms' ), + 'order.deleted' => __( 'Order deleted', 'lifterlms' ), + 'order.restored' => __( 'Order restored', 'lifterlms' ), + 'transaction.created' => __( 'Transaction created', 'lifterlms' ), + 'transaction.updated' => __( 'Transaction updated', 'lifterlms' ), + 'transaction.deleted' => __( 'Transaction deleted', 'lifterlms' ), + 'student.created' => __( 'Student created', 'lifterlms' ), + 'student.updated' => __( 'Student updated', 'lifterlms' ), + 'student.deleted' => __( 'Student deleted', 'lifterlms' ), + 'enrollment.created' => __( 'Enrollment created', 'lifterlms' ), + 'enrollment.updated' => __( 'Enrollment updated', 'lifterlms' ), + 'enrollment.deleted' => __( 'Enrollment deleted', 'lifterlms' ), + 'progress.updated' => __( 'Progress updated', 'lifterlms' ), + 'progress.deleted' => __( 'Progress deleted', 'lifterlms' ), + 'instructor.created' => __( 'Instructor created', 'lifterlms' ), + 'instructor.updated' => __( 'Instructor updated', 'lifterlms' ), + 'instructor.deleted' => __( 'Instructor deleted', 'lifterlms' ), + 'action' => __( 'Action', 'lifterlms' ), + ) + ); + + } + + /** + * Retrieve a list of hooks for each topic. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.11 `'save_post_*'` hooks number of arguments reduced to two. + * + * @return array + */ + public function get_hooks() { + + $hooks = array( + + // Courses. + 'course.created' => array( + 'save_post_course' => 2, + ), + 'course.updated' => array( + 'edit_post_course' => 2, + ), + 'course.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + 'course.restored' => array( + 'untrashed_post' => 1, + ), + + // Sections. + 'section.created' => array( + 'save_post_section' => 2, + ), + 'section.updated' => array( + 'edit_post_section' => 2, + ), + 'section.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + + // Lessons. + 'lesson.created' => array( + 'save_post_lesson' => 2, + ), + 'lesson.updated' => array( + 'edit_post_lesson' => 2, + ), + 'lesson.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + 'lesson.restored' => array( + 'untrashed_post' => 1, + ), + + // Memberships. + 'membership.created' => array( + 'save_post_llms_membership' => 2, + ), + 'membership.updated' => array( + 'edit_post_llms_membership' => 2, + ), + 'membership.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + 'membership.restored' => array( + 'untrashed_post' => 1, + ), + + // Access Plans. + 'access_plan.created' => array( + 'save_post_llms_access_plan' => 2, + ), + 'access_plan.updated' => array( + 'edit_post_llms_access_plan' => 2, + ), + 'access_plan.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + + // Orders. + 'order.created' => array( + 'save_post_llms_order' => 2, + ), + 'order.updated' => array( + 'edit_post_llms_order' => 2, + ), + 'order.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + + // Transactions. + 'transaction.created' => array( + 'save_post_llms_transaction' => 2, + ), + 'transaction.updated' => array( + 'edit_post_llms_transaction' => 2, + ), + 'transaction.deleted' => array( + 'wp_trash_post' => 1, + 'delete_post' => 1, + ), + + // Students. + 'student.created' => array( + 'user_register' => 1, + 'lifterlms_user_registered' => 1, + ), + 'student.updated' => array( + 'profile_update' => 1, + 'lifterlms_user_updated' => 1, + ), + 'student.deleted' => array( + 'delete_user' => 1, + ), + + // Instructors. + 'instructor.created' => array( + 'user_register' => 1, + ), + 'instructor.updated' => array( + 'profile_update' => 1, + ), + 'instructor.deleted' => array( + 'delete_user' => 1, + ), + + 'enrollment.created' => array( + 'llms_user_course_enrollment_created' => 2, + 'llms_user_membership_enrollment_created' => 2, + ), + 'enrollment.updated' => array( + 'llms_user_course_enrollment_updated' => 2, + 'llms_user_membership_enrollment_updated' => 2, + 'llms_user_removed_from_course' => 2, + 'llms_user_removed_from_membership_level' => 2, + ), + 'enrollment.deleted' => array( + 'llms_user_enrollment_deleted' => 2, + ), + + 'progress.updated' => array( + 'llms_mark_complete' => 2, + 'llms_mark_incomplete' => 2, + ), + // 'progress.deleted' => array(), + + ); + + return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks ); + + } + + /** + * Retrieve an array of supported post types used as resources for webhooks. + * + * @since 1.0.0-beta.1 + * + * @return string[] + */ + public function get_post_type_resources() { + + /** + * Filter the list of supported post types used as resources for webhooks. + * + * @param string[] $post_types Array of post type names. + */ + return apply_filters( + 'llms_rest_get_post_type_resources', + array( + 'course', + 'section', + 'lesson', + 'llms_membership', + 'llms_access_plan', + 'llms_order', + 'llms_transaction', + ) + ); + + } + + /** + * Validate data supplied for creating/updating a key. + * + * @since 1.0.0-beta.1 + * + * @param array $data Associative array of data to set to a key's properties. + * @return WP_Error|true When data is invalid will return a WP_Error with information about the invalid properties, + * otherwise `true` denoting data is valid. + */ + protected function is_data_valid( $data ) { + + // Validate Status. + if ( isset( $data['status'] ) && ! in_array( $data['status'], array_keys( $this->get_statuses() ), true ) ) { + // Translators: %s = Invalid permission string. + return new WP_Error( 'llms_rest_webhook_invalid_status', sprintf( __( '"%s" is not a valid status.', 'lifterlms' ), $data['status'] ) ); + } + + // Validate Topics. + if ( isset( $data['topic'] ) && ! $this->is_topic_valid( $data['topic'] ) ) { + // Translators: %s = Invalid permission string. + return new WP_Error( 'llms_rest_webhook_invalid_topic', sprintf( __( '"%s" is not a valid topic.', 'lifterlms' ), $data['topic'] ) ); + } + + // Prevent empty / blank values being passed. + foreach ( array( 'name', 'delivery_url' ) as $key ) { + if ( isset( $data[ $key ] ) && empty( $data[ $key ] ) ) { + // Translators: %s = field name. + return new WP_Error( 'llms_rest_webhook_invalid_' . $key, sprintf( __( '"%s" is required.', 'lifterlms' ), $key ) ); + } + } + + return true; + + } + + /** + * Determine if a given topic is valid. + * + * @since 1.0.0-beta.1 + * + * @param string $topic Topic. + * @return bool + */ + public function is_topic_valid( $topic ) { + + $split = explode( '.', $topic ); + + if ( 'action' === $split[0] && ! empty( $split[1] ) ) { + return true; + } elseif ( in_array( $topic, array_keys( $this->get_topics() ), true ) && 'action' !== $topic ) { + return true; + } + + return false; + + } + + /** + * Load webhooks + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 + * + * @return int Number of hooks loaded. + */ + public function load() { + + /** + * Limit the number of webhooks that are loaded. By default all webhooks are loaded. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.16 When retrieving the webhooks, instantiate the webhooks query passing `no_found_rows` arg as `true`, + * to improve performance (no pagination is needed). + * @param int $limit Number of webhooks to load. Default `null` loads all webhooks. + */ + $limit = apply_filters( 'llms_load_webhooks_limit', null ); + + $hooks = new LLMS_REST_Webhooks_Query( + array( + 'status' => 'active', + 'per_page' => $limit ? $limit : 999, + 'no_found_rows' => true, + ) + ); + + $loaded = 0; + foreach ( $hooks->get_webhooks() as $hook ) { + $hook->enqueue(); + $loaded++; + } + + return $loaded; + + } + + /** + * Persist data. + * + * This method assumes the supplied data has already been validated and sanitized. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_REST_Webhook $obj Webhook object. + * @param array $data Associative array of data to persist. + * @return obj + */ + protected function save( $obj, $data ) { + + if ( isset( $data['delivery_url'] ) && ( ! $obj->exists() || $obj->exists() && $data['delivery_url'] !== $obj->get( 'delivery_url' ) ) ) { + $obj->set( 'delivery_url', $data['delivery_url'] ); + $ping = $obj->ping(); + if ( is_wp_error( $ping ) ) { + return $ping; + } + } + + $obj->setup( $data )->save(); + return $obj; + + } + + /** + * Prepare data for an update. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column. + * + * @param array $data Associative array of data to set to a resources properties. + * @return LLMS_REST_Webhook|WP_Error + */ + protected function update_prepare( $data ) { + + $url = isset( $data['delivery_url'] ); + + // Merge in (some) default values. + $defaults = $this->get_default_column_values(); + unset( $defaults['failure_count'] ); + $data = wp_parse_args( array_filter( $data ), $defaults ); + + // URL was supplied but empty so add it back in to get caught by validation. + if ( $url && ! isset( $data['delivery_url'] ) ) { + $data['delivery_url'] = ''; + } + + // Validate via default parent methods. + $data = parent::update_prepare( $data ); + + if ( is_wp_error( $data ) ) { + return $data; + } + + // Add updated date. + $data['updated'] = llms_current_time( 'mysql' ); + + return $data; + + } + +} diff --git a/libraries/lifterlms-rest/includes/index.php b/libraries/lifterlms-rest/includes/index.php new file mode 100644 index 0000000000..bf834a27df --- /dev/null +++ b/libraries/lifterlms-rest/includes/index.php @@ -0,0 +1,2 @@ +<?php +// quiet you. diff --git a/libraries/lifterlms-rest/includes/llms-rest-functions.php b/libraries/lifterlms-rest/includes/llms-rest-functions.php new file mode 100644 index 0000000000..37bda9f2b9 --- /dev/null +++ b/libraries/lifterlms-rest/includes/llms-rest-functions.php @@ -0,0 +1,85 @@ +<?php +/** + * REST functions + * + * @package LifterLMS_REST/Functions + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.2 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Generate a keyed hash value using the HMAC method with the key `llms-rest-api` + * + * @since 1.0.0-beta.1 + * + * @param string $data Message to be hashed. + * @return string + */ +function llms_rest_api_hash( $data ) { + return hash_hmac( 'sha256', $data, 'llms-rest-api' ); +} + +/** + * Wrapper function to execute async delivery of webhooks. + * + * Hooked to `lifterlms_rest_deliver_webhook_async`. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.2 Fixed incorrect reference. + * + * @see LLMS_REST_Webhook::schedule() + * + * @param int $webhook_id Webhook id. + * @param array $args Numeric array of arguments from the originating hook. + * @return void + */ +function llms_rest_deliver_webhook_async( $webhook_id, $args ) { + + $webhook = LLMS_REST_API()->webhooks()->get( $webhook_id ); + if ( $webhook ) { + $webhook->deliver( $args ); + } + +} +add_action( 'lifterlms_rest_deliver_webhook_async', 'llms_rest_deliver_webhook_async', 10, 2 ); + +/** + * Get data from a WP Rest API endpoint. + * + * @since 1.0.0-beta.1 + * + * @param string $endpoint API endpoint, eg "/llms/v1/courses". + * @param array $params Query params to add to the request. + * @return array|WP_Error + */ +function llms_rest_get_api_endpoint_data( $endpoint, $params = array() ) { + + $req = new WP_Rest_Request( 'GET', $endpoint ); + if ( $params ) { + $req->set_query_params( $params ); + } + + $res = rest_do_request( $req ); + $server = rest_get_server(); + $json = wp_json_encode( $server->response_to_data( $res, false ) ); + + return json_decode( $json, true ); + +} + +/** + * Generate a random hash. + * + * @since 1.0.0-beta.1 + * + * @return string + */ +function llms_rest_random_hash() { + if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) { + return sha1( wp_rand() ); + } + return bin2hex( openssl_random_pseudo_bytes( 20 ) ); +} diff --git a/libraries/lifterlms-rest/includes/models/class-llms-rest-api-key.php b/libraries/lifterlms-rest/includes/models/class-llms-rest-api-key.php new file mode 100644 index 0000000000..f6c1d796ca --- /dev/null +++ b/libraries/lifterlms-rest/includes/models/class-llms-rest-api-key.php @@ -0,0 +1,178 @@ +<?php +/** + * API Key Model. + * + * @package LifterLMS_REST/Models + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_API_Key class.. + * + * @since 1.0.0-beta.1 + */ +class LLMS_REST_API_Key extends LLMS_Abstract_Database_Store { + + /** + * Date Created Field not implemented. + * + * @var null + */ + protected $date_created = null; + + /** + * Date Updated Field not implemented. + * + * @var null + */ + protected $date_updated = null; + + /** + * Array of table column name => format + * + * @var array + */ + protected $columns = array( + 'user_id' => '%d', + 'description' => '%s', + 'consumer_key' => '%s', + 'consumer_secret' => '%s', + 'truncated_key' => '%s', + 'last_access' => '%s', + ); + + /** + * Database Table Name + * + * @var string + */ + protected $table = 'api_keys'; + + /** + * The record type + * + * Used for filters/actions. + * + * @var string + */ + protected $type = 'rest_api_key'; + + /** + * Constructor + * + * @since 1.0.0-beta.1 + * + * @param int $id API Key ID. + * @param bool $hydrate If true, hydrates the object on instantiation if an ID is supplied. + */ + public function __construct( $id = null, $hydrate = true ) { + + $this->id = $id; + if ( $this->id && $hydrate ) { + $this->hydrate(); + } + + } + + /** + * Retrieve an admin nonce url for deleting an API key. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_delete_link() { + + return add_query_arg( + array( + 'revoke-key' => $this->get( 'id' ), + 'key-revoke-nonce' => wp_create_nonce( 'revoke' ), + ), + LLMS_REST_API()->keys()->get_admin_url() + ); + + } + + + /** + * Retrieve the admin URL where the api key is managed. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_edit_link() { + return add_query_arg( + array( + 'edit-key' => $this->get( 'id' ), + ), + LLMS_REST_API()->keys()->get_admin_url() + ); + } + + /** + * Retrieve a human-readable date/time string for the date the key was last used. + * + * Uses WP Core date & time formatting settings. + * + * @since 1.0.0-beta.1 + * + * @return string + */ + public function get_last_access_date() { + + $date = __( 'None', 'lifterlms' ); + if ( ! empty( $this->get( 'last_access' ) ) ) { + $time = strtotime( $this->get( 'last_access' ) ); + // Translators: %1$s: Last access date; %2$s: Last access time. + $date = sprintf( __( '%1$s at %2$s', 'lifterlms' ), date_i18n( get_option( 'date_format' ), $time ), date_i18n( get_option( 'time_format' ), $time ) ); + } + + return apply_filters( 'llms_rest_api_key_get_last_access_date', $date, $this ); + + } + + /** + * Determine if the key has the permissions required by the HTTP Request Method. + * + * @since 1.0.0-beta.1 + * + * @param string $method The HTTP request method. + * @return bool + */ + public function has_permission( $method ) { + + $permissions = $this->get( 'permissions' ); + + switch ( $method ) { + case 'HEAD': + case 'GET': + $ret = ( 'read' === $permissions || 'read_write' === $permissions ); + break; + + case 'POST': + case 'PUT': + case 'PATCH': + case 'DELETE': + $ret = ( 'write' === $permissions || 'read_write' === $permissions ); + break; + + case 'OPTIONS': + $ret = true; + break; + + default: + $ret = false; + + } + + return $ret; + + } + + +} diff --git a/libraries/lifterlms-rest/includes/models/class-llms-rest-webhook.php b/libraries/lifterlms-rest/includes/models/class-llms-rest-webhook.php new file mode 100644 index 0000000000..08d8a9fcae --- /dev/null +++ b/libraries/lifterlms-rest/includes/models/class-llms-rest-webhook.php @@ -0,0 +1,501 @@ +<?php +/** + * Webhook Model + * + * @package LifterLMS_REST/Models + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.11 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Webhook class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.11 When validating a resource: + * - Skipped autosaves and revisions. + * - Implemented a new way to consider a resource as just created. Thanks WooCoommerce. + */ +class LLMS_REST_Webhook extends LLMS_REST_Webhook_Data { + + /** + * Store which object IDs this webhook has processed (ie scheduled to be delivered) + * within the current page request. + * + * @var array + */ + protected $processed = array(); + + /** + * Delivers the webhook + * + * @since 1.0.0-beta.1 + * + * @param array $args Numeric array of arguments from the originating hook. + * @return void + */ + public function deliver( $args ) { + + $start = microtime( true ); + $payload = $this->get_payload( $args ); + + $http_args = array( + 'method' => 'POST', + 'timeout' => 60, + 'redirection' => 0, + 'user-agent' => $this->get_user_agent(), + 'body' => trim( wp_json_encode( $payload ) ), + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ); + + /** + * Modify HTTP args used to deliver the webhook + * + * @since 1.0.0-beta.1 + * + * @param array $http_args HTTP request args suitable for `wp_remote_request()`. + * @param LLMS_REST_Webhook $this Webhook object. + * @param mixed $args First argument passed to the action triggering the webhook. + */ + $http_args = apply_filters( 'llms_rest_webhook_delivery_args', $http_args, $this, $args ); + + $delivery_id = wp_hash( $this->get( 'id' ) . strtotime( 'now' ) ); + + $http_args['headers'] = array_merge( + $http_args['headers'], + array( + 'X-LLMS-Webhook-Source' => home_url( '/' ), + 'X-LLMS-Webhook-Topic' => $this->get( 'topic' ), + 'X-LLMS-Webhook-Resource' => $this->get_resource(), + 'X-LLMS-Webhook-Event' => $this->get_event(), + 'X-LLMS-Webhook-Signature' => $this->get_delivery_signature( $http_args['body'] ), + 'X-LLMS-Webhook-ID' => $this->get( 'id' ), + 'X-LLMS-Delivery-ID' => $delivery_id, + ) + ); + + $res = wp_safe_remote_request( $this->get( 'delivery_url' ), $http_args ); + + $duration = round( microtime( true ) - $start, 5 ); + + $this->delivery_after( $delivery_id, $http_args, $res, $duration ); + + /** + * Fires after a webhook is delivered + * + * @since 1.0.0-beta.1 + * + * @param array $http_args HTTP request args. + * @param WP_Error|array $res Remote response. + * @param int $duration Executing time. + * @param array $args Numeric array of arguments from the originating hook. + * @param LLMS_REST_Webhook $this Webhook object. + */ + do_action( 'llms_rest_webhook_delivery', $http_args, $res, $duration, $args, $this ); + + } + + /** + * Fires after delivery + * + * Logs data when loggind enabled and updates state data. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Stop setting the webhook's property `pending_delivery` to 0. + * We now rely on the method `is_already_processed()` to determine whether the webhook delivering should be avoided. + * + * @param string $delivery_id Webhook delivery id (for logging). + * @param array $req_args HTTP Request Arguments used to deliver the webhook. + * @param array $res Results from `wp_safe_remote_request()`. + * @param float $duration Time (in microseconds) it took to generate and deliver the webhook. + * @return void + */ + protected function delivery_after( $delivery_id, $req_args, $res, $duration ) { + + // Parse response. + if ( is_wp_error( $res ) ) { + $res_code = $res->get_error_code(); + $res_message = $res->get_error_message(); + $res_headers = array(); + $res_body = ''; + } else { + $res_code = wp_remote_retrieve_response_code( $res ); + $res_message = wp_remote_retrieve_response_message( $res ); + $res_headers = wp_remote_retrieve_headers( $res ); + $res_body = wp_remote_retrieve_body( $res ); + } + + if ( defined( 'LLMS_REST_WEBHOOK_DELIVERY_LOGGING' ) && LLMS_REST_WEBHOOK_DELIVERY_LOGGING ) { + + $message = array( + 'Delivery ID' => $delivery_id, + 'Date' => date_i18n( __( 'M j, Y @ H:i', 'lifterlms' ), strtotime( 'now' ), true ), + 'URL' => $this->get( 'delivery_url' ), + 'Duration' => $duration, + 'Request' => array( + 'Method' => $req_args['method'], + 'Headers' => array_merge( + array( + 'User-Agent' => $req_args['user-agent'], + ), + $req_args['headers'] + ), + ), + 'Body' => wp_slash( $req_args['body'] ), + 'Response' => array( + 'Code' => $res_code, + 'Message' => $res_message, + 'Headers' => $res_headers, + 'Body' => $res_body, + ), + ); + + if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { + $message['Webhook Delivery']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on.'; + $message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on.'; + } + + llms_log( $message, sprintf( 'webhook-%d', $this->get( 'id' ) ) ); + + } + + // Check for a success, which is a 2xx, 301 or 302 Response Code. + if ( absint( $res_code ) >= 200 && absint( $res_code ) <= 302 ) { + $this->set( 'failure_count', 0 ); + } else { + $this->set_delivery_failure(); + } + + } + + /** + * Add actions for all the webhooks hooks + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function enqueue() { + + foreach ( $this->get_hooks() as $hook => $args ) { + add_action( $hook, array( $this, 'process_hook' ), 10, $args ); + } + + } + + /** + * Checks if the specified resource has already been queued for delivery within the current request + * + * Helps avoid duplication of data being sent for topics that have more than one hook defined. + * + * @param array $args Numeric array of arguments from the originating hook. + * @return bool + */ + protected function is_already_processed( $args ) { + return false !== array_search( $args[0], $this->processed, true ); + } + + /** + * Determine if the current action is valid for the webhook + * + * @since 1.0.0-beta.1 + * + * @param array $args Numeric array of arguments from the originating hook. + * @return bool + */ + protected function is_valid_action( $args ) { + + $ret = true; + switch ( current_action() ) { + + case 'wp_trash_post': + case 'delete_post': + case 'untrashed_post': + $ret = $this->is_valid_post_action( $args[0] ); + break; + + case 'user_register': + case 'profile_update': + case 'delete_user': + $ret = $this->is_valid_user_action( $args[0] ); + break; + + } + + /** + * Determine if the current action is valid for the webhook + * + * @param bool $ret Whether or not the action is valid. + * @param array $args Numeric array of arguments from the originating hook. + * @param LLMS_REST_Webhook $this Webhook object. + */ + return apply_filters( 'llms_rest_webhook_is_valid_action', $ret, $args, $this ); + + } + + /** + * Determine if the current post-related action is valid for the webhook + * + * @since 1.0.0-beta.1 + * + * @param int $post_id WP Post ID. + * @return bool + */ + protected function is_valid_post_action( $post_id ) { + + $post_type = get_post_type( $post_id ); + + // Check the post type is a supported post type. + if ( ! in_array( get_post_type( $post_id ), LLMS_REST_API()->webhooks()->get_post_type_resources(), true ) ) { + return false; + } + + // Ensure the current action matches the resource for the current webhook. + if ( str_replace( 'llms_', '', $post_type ) !== $this->get_resource() ) { + return false; + } + + return true; + + } + + /** + * Determine if the the resource is valid for the webhook + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.11 Skipped autosaves and revisions. + * Implemented a new way to consider a resource as just created. Thanks WooCoommerce. + * + * @param array $args Numeric array of arguments from the originating hook. + * @return bool + */ + protected function is_valid_resource( $args ) { + + $resource = $this->get_resource(); + + if ( in_array( $resource, LLMS_REST_API()->webhooks()->get_post_type_resources(), true ) ) { + + $post_resource = get_post( absint( $args[0] ) ); + + // Ignore auto-drafts. + if ( in_array( get_post_status( $post_resource ), array( 'new', 'auto-draft' ), true ) ) { + return false; + } + + if ( false !== strpos( current_action(), 'save_post' ) || false !== strpos( current_action(), 'edit_post' ) ) { + + // Ignore autosaves and revisions. + if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || is_int( wp_is_post_revision( $post_resource ) ) || is_int( wp_is_post_autosave( $resource ) ) ) { + return false; + } + + // Drafts don't have post_date_gmt so calculate it here. + $gmt_date = get_gmt_from_date( $post_resource->post_date ); + + // A resource is considered created when the hook is executed within 10 seconds of the post creation date. + $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); + + $event = $this->get_event(); + + if ( ( 'created' === $event && false !== strpos( current_action(), 'save_post' ) ) && ! $resource_created ) { + return false; + } elseif ( ( 'updated' === $event && false !== strpos( current_action(), 'edit_post' ) ) && $resource_created ) { + return false; + } + } + } + + return true; + + } + + /** + * Determine if the current user-related action is valid for the webhook + * + * @since 1.0.0-beta.1 + * + * @param int $user_id WP User ID. + * @return bool + */ + protected function is_valid_user_action( $user_id ) { + + $user = get_userdata( $user_id ); + + if ( ! $user ) { + return false; + } + + $resource = $this->get_resource(); + if ( 'student' === $resource && ! in_array( 'student', (array) $user->roles, true ) ) { + return false; + } elseif ( 'instructor' === $resource && ! user_can( $user_id, 'lifterlms_instructor' ) ) { + return false; + } + + return true; + + } + + /** + * Processes information from the origination action hook + * + * Determines if the webhook should be delivered and whether or not it should be scheduled or delivered immediately. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Mark this hook's first argument as processed to ensure it doesn't get processed again within the current request. + * And don't rely anymore on the webhook's `pending_delivery` property to achieve the same goal. + * + * @param mixed ...$args Arguments from the hook. + * @return int|false Timestamp of the scheduled event when the webhook is successfully scheduled. + * false if the webhook should not be delivered or has already been delivered in the last 5 minutes. + */ + public function process_hook( ...$args ) { + + if ( ! $this->should_deliver( $args ) ) { + return false; + } + + // Mark this hook's first argument as processed to ensure it doesn't get processed again within the current request, + // as it might happen with webhooks with multiple hookes defined in `LLMS_REST_Webhooks::get_hooks()`. + $this->processed[] = $args[0]; + + /** + * Disable background processing of webhooks by returning a falsy + * + * Note: disabling async processing may create delays for users of your site. + * + * @param bool $async Whether async processing is enabled or not. + * @param LLMS_REST_Webhook $this Webhook object. + * @param array $args Numeric array of arguments from the originating hook. + */ + if ( apply_filters( 'llms_rest_webhook_deliver_async', true, $this, $args ) ) { + return $this->schedule( $args ); + } + + return $this->deliver( $args ); + + } + + /** + * Perform a test ping to the delivery url + * + * @since 1.0.0-beta.1 + * + * @return true|WP_Error + */ + public function ping() { + + $pre = apply_filters( 'llms_rest_webhook_pre_ping', false, $this->get( 'id' ) ); + if ( false !== $pre ) { + return $pre; + } + + $ping = wp_safe_remote_post( + $this->get( 'delivery_url' ), + array( + 'user-agent' => $this->get_user_agent(), + 'body' => sprintf( 'webhook_id=%d', $this->get( 'id' ) ), + ) + ); + + $res_code = wp_remote_retrieve_response_code( $ping ); + + if ( is_wp_error( $ping ) ) { + // Translators: %s = Error message. + return new WP_Error( 'llms_rest_webhook_ping_unreachable', sprintf( __( 'Could not reach the delivery url: "%s".', 'lifterlms' ), $ping->get_error_message() ) ); + } + + if ( 200 !== $res_code ) { + // Translators: %d = Response code. + return new WP_Error( 'llms_rest_webhook_ping_not_200', sprintf( __( 'The delivery url returned the response code "%d".', 'lifterlms' ), absint( $res_code ) ) ); + } + + return true; + + } + + /** + * Determines if an originating action qualifies for webhook delivery + * + * @since 1.0.0-beta.1 + * @since [verison] Drop checking whether the webhook is pending in favor of a check on if is already processed within the current request. + * + * @param array $args Numeric array of arguments from the originating hook. + * @return bool + */ + protected function should_deliver( $args ) { + + $deliver = ( 'active' === $this->get( 'status' ) ) // Must be active. + && $this->is_valid_action( $args ) // Valid action. + && $this->is_valid_resource( $args ) // Valid resource. + && ! $this->is_already_processed( $args ); // Not already processed. + + /** + * Skip or hijack webhook delivery scheduling + * + * @param bool $deliver Whether or not to deliver webhook delivery. + * @param LLMS_REST_Webhook $this Webhook object. + * @param array $args Numeric array of arguments from the originating hook. + */ + return apply_filters( 'llms_rest_webhook_should_deliver', $deliver, $this, $args ); + + } + + /** + * Schedule the webhook for async delivery + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.17 Stop setting the webhook's property `pending_delivery` to 1 when scheduling the delivery. + * We now rely on the method `is_already_processed()` to determine whether the webhook scheduling should be avoided. + * + * @param array $args Numeric array of arguments from the originating hook. + * @return bool + */ + protected function schedule( $args ) { + + // Remove object & array arguments before scheduling to avoid hitting column index size issues imposed by the ActionScheduler lib. + foreach ( $args as $index => &$arg ) { + if ( is_array( $arg ) || is_object( $arg ) ) { + $arg = null; + } + } + + $schedule_args = array( + 'webhook_id' => $this->get( 'id' ), + 'args' => $args, + ); + + $next = as_next_scheduled_action( 'lifterlms_rest_deliver_webhook_async', $schedule_args, 'llms-webhooks' ); + + /** + * Determines the time period required to wait between delivery of the webhook + * + * If the webhook has already been scheduled within this time period it will not be sent again + * until the period expires. For example, the default time period is 300 seconds (5 minutes). + * If the webhook is triggered at 12:00pm it will be scheduled. If it is triggered again at 12:03pm the + * second occurrence will not be scheduled. If it is triggerd again at 12:06pm this third occurrence will + * again be scheduled. + * + * @since 1.0.0-beta.1 + * + * @param int $delay Time (in seconds). + * @param array $args Numeric array of arguments from the originating hook. + * @param LLMS_REST_Webhook $this Webhook object. + */ + $delay = apply_filters( 'llms_rest_webhook_repeat_delay', 300, $args, $this ); + + if ( ! $next || $next >= ( $delay + gmdate( 'U' ) ) ) { + + return as_schedule_single_action( time(), 'lifterlms_rest_deliver_webhook_async', $schedule_args, 'llms-webhooks' ) ? true : false; + + } + + return false; + + } + +} diff --git a/libraries/lifterlms-rest/includes/models/index.php b/libraries/lifterlms-rest/includes/models/index.php new file mode 100644 index 0000000000..9c65c1efa6 --- /dev/null +++ b/libraries/lifterlms-rest/includes/models/index.php @@ -0,0 +1 @@ +<?php // shhh. diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-access-plans-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-access-plans-controller.php new file mode 100644 index 0000000000..8f4ba60f1d --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-access-plans-controller.php @@ -0,0 +1,774 @@ +<?php +/** + * REST Access Plans Controller + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.18 + * @version 1.0.0-beta.20 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Access_Plans_Controller class + * + * @since 1.0.0-beta.18 + */ +class LLMS_REST_Access_Plans_Controller extends LLMS_REST_Posts_Controller { + + /** + * Post type + * + * @var string + */ + protected $post_type = 'llms_access_plan'; + + /** + * Route base + * + * @var string + */ + protected $rest_base = 'access-plans'; + + /** + * Get the Access Plan's schema, conforming to JSON Schema + * + * @since 1.0.0-beta.18 + * + * @return array + */ + public function get_item_schema() { + + $schema = (array) parent::get_item_schema(); + + // Post properties to unset. + $properties_to_unset = array( + 'comment_status', + 'excerpt', + 'featured_media', + 'password', + 'ping_status', + 'slug', + 'status', + ); + foreach ( $properties_to_unset as $to_unset ) { + unset( $schema['properties'][ $to_unset ] ); + } + + // The content is not required. + unset( $schema['properties']['content']['required'] ); + + $access_plan_properties = require LLMS_REST_API_PLUGIN_DIR . 'includes/server/schemas/schema-access-plans.php'; + + $schema['properties'] = array_merge( + $schema['properties'], + $access_plan_properties + ); + + /** + * Filter item schema for the access-plan controller + * + * @since 1.0.0-beta.18 + * + * @param array $schema Item schema data. + */ + return apply_filters( 'llms_rest_access_plan_item_schema', $schema ); + + } + + /** + * Retrieves the query params for the objects collection + * + * @since 1.0.0-beta.18 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $query_params = parent::get_collection_params(); + + $query_params['post_id'] = array( + 'description' => __( 'Retrieve access plans for a specific list of one or more posts. Accepts a course/membership id or comma separated list of course/membership ids.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Retrieves an array of arguments for the delete endpoint + * + * @since 1.0.0-beta.18 + * + * @return array Delete endpoint arguments. + */ + public function get_delete_item_args() { + return array(); + } + + /** + * Whether the delete should be forced + * + * @since 1.0.0-beta.18 + * + * @param WP_REST_Request $request Full details about the request. + * @return bool True if the delete should be forced, false otherwise. + */ + protected function is_delete_forced( $request ) { + return true; + } + + /** + * Whether the trash is supported + * + * @since 1.0.0-beta.18 + * + * @return bool True if the trash is supported, false otherwise. + */ + protected function is_trash_supported() { + return false; + } + + /** + * Check if a given request has access to create an item + * + * @since 1.0.0-beta.18 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + + $can_create = parent::create_item_permissions_check( $request ); + + // If current user cannot create the item because of authorization, check if the current user can edit the "parent" course/membership. + $can_create = $this->related_product_permissions_check( $can_create, $request ); + + return is_wp_error( $can_create ) ? $can_create : $this->allow_request_when_access_plan_limit_not_reached( $request ); + } + + /** + * Check if a given request has access to update an item + * + * @since 1.0.0-beta.18 + * @since 1.0.0-beta.20 Call to private method `block_request_when_access_plan_limit` replaced with a call to the new `allow_request_when_access_plan_limit_not_reached` method. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + + $can_update = parent::update_item_permissions_check( $request ); + + // If current user cannot edit the item because of authorization, check if the current user can edit the "parent" course/membership. + $can_update = $this->related_product_permissions_check( $can_update, $request ); + + return is_wp_error( $can_update ) ? $can_update : $this->allow_request_when_access_plan_limit_not_reached( $request ); + + } + + /** + * Check if a given request has access to delete an item + * + * @since 1.0.0-beta.18 + * @since 1.0.0-beta.20 Call to private method `block_request_when_access_plan_limit` replaced with a call to the new `allow_request_when_access_plan_limit_not_reached` method. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + $can_delete = parent::delete_item_permissions_check( $request ); + + // If current user cannot delete the item because of authorization, check if the current user can edit the "parent" course/membership. + return $this->related_product_permissions_check( $can_delete, $request ); + + } + + /** + * Prepare links for the request + * + * @since 1.0.0-beta.18 + * + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $access_plan, $request ) { + + $links = parent::prepare_links( $access_plan, $request ); + unset( $links['content'] ); + + $links['post'] = array( + 'href' => rest_url( + sprintf( + '%s/%s/%s', + 'llms/v1', + 'course' === $access_plan->get_product_type() ? 'courses' : 'memberships', + $access_plan->get( 'product_id' ) + ) + ), + ); + + // Membership restrictions. + if ( $access_plan->has_availability_restrictions() ) { + $links['restrictions'] = array( + 'href' => rest_url( + sprintf( + '%s/%s?include=%s', + 'llms/v1', + 'memberships', + implode( ',', $access_plan->get_array( 'availability_restrictions' ) ) + ) + ), + ); + } + + /** + * Filters the access plan's links. + * + * @since 1.0.0-beta.18 + * + * @param array $links Links for the given access plan. + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + */ + return apply_filters( 'llms_rest_access_plan_links', $links, $access_plan ); + + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.18 + * @since 1.0.0-beta.20 Fixed return format of the `access_expires` property. + * Fixed sale date properties. + * + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $access_plan, $request ) { + + $data = parent::prepare_object_for_response( $access_plan, $request ); + $context = $request->get_param( 'context' ); + + // Price. + $data['price'] = $access_plan->is_free() ? 0 : $access_plan->get_price( 'price', array(), 'float' ); + + // Access expiration. + $data['access_expiration'] = $access_plan->get( 'access_expiration' ); + + // Access expires date. + if ( 'limited-date' === $data['access_expiration'] || 'edit' === $context ) { + $data['access_expires'] = $access_plan->get_date( 'access_expires', 'Y-m-d H:i:s' ); + } + + // Access length and period. + if ( 'limited-period' === $data['access_expiration'] || 'edit' === $context ) { + $data['access_length'] = $access_plan->get( 'access_length' ); + $data['access_period'] = $access_plan->get( 'access_period' ); + } + + // Availability restrictions. + $data['availability_restrictions'] = $access_plan->has_availability_restrictions() ? array() : $access_plan->get_array( 'availability_restrictions' ); + + // Enroll text. + $data['enroll_text'] = $access_plan->get_enroll_text(); + + // Frequency. + $data['frequency'] = $access_plan->get( 'frequency' ); + + // Length and period. + if ( 0 < $data['frequency'] || 'edit' === $context ) { + $data['length'] = $access_plan->get( 'length' ); + $data['period'] = $access_plan->get( 'period' ); + } + + // Post ID. + $data['post_id'] = $access_plan->get( 'product_id' ); + + // Redirect forced. + if ( ! empty( $data['availability_restrictions'] ) || 'edit' === $context ) { + $data['redirect_forced'] = llms_parse_bool( $access_plan->get( 'checkout_redirect_forced' ) ); + } + + // Redirect type. + $data['redirect_type'] = $access_plan->get( 'checkout_redirect_type' ); + + // Redirect page. + if ( 'page' === $data['redirect_type'] || 'edit' === $context ) { + $data['redirect_page'] = $access_plan->get( 'checkout_redirect_page' ); + } + + // Redirect url. + if ( 'url' === $data['redirect_type'] || 'edit' === $context ) { + $data['redirect_url'] = $access_plan->get( 'checkout_redirect_url' ); + } + + // Permalink. + $data['permalink'] = $access_plan->get_checkout_url( false ); + + // Sale enabled. + $data['sale_enabled'] = llms_parse_bool( $access_plan->get( 'on_sale' ) ); + + // Sale start/end and price. + if ( $data['sale_enabled'] || 'edit' === $context ) { + $data['sale_date_start'] = $access_plan->get_date( 'sale_start', 'Y-m-d H:i:s' ); + $data['sale_date_end'] = $access_plan->get_date( 'sale_end', 'Y-m-d H:i:s' ); + $data['sale_price'] = $access_plan->get_price( 'sale_price', array(), 'float' ); + } + + // SKU. + $data['sku'] = $access_plan->get( 'sku' ); + + // Trial. + $data['trial_enabled'] = $access_plan->has_trial(); + + if ( $data['trial_enabled'] || 'edit' === $context ) { + $data['trial_length'] = $access_plan->get( 'trial_length' ); + $data['trial_period'] = $access_plan->get( 'trial_period' ); + $data['trial_price'] = $access_plan->get_price( 'trial_price', array(), 'float' ); + } + + // Visibility. + $data['visibility'] = $access_plan->get_visibility(); + + /** + * Filters the access plan data for a response. + * + * @since 1.0.0-beta.18 + * + * @param array $data Array of lesson properties prepared for response. + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + * @param WP_REST_Request $request Full details about the request. + */ + $data = apply_filters( 'llms_rest_prepare_access_plan_object_response', $data, $access_plan, $request ); + + return $data; + } + + /** + * Format query arguments to retrieve a collection of objects + * + * @since 1.0.0-beta.18 + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $query_args = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $query_args ) ) { + return $query_args; + } + + // Filter by post ID. + if ( ! empty( $request['post_id'] ) ) { + $query_args = array_merge( + $query_args, + array( + 'meta_query' => array( + array( + 'key' => '_llms_product_id', + 'value' => $request['post_id'], + 'compare' => 'IN', + ), + ), + ) + ); + } + + return $query_args; + } + + /** + * Prepares a single post for create or update + * + * @since 1.0.0-beta.18 + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of llms post args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + + $prepared_item = parent::prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_item ) ) { + return $prepared_item; + } + + $schema = $this->get_item_schema(); + + // Enroll text. + if ( ! empty( $schema['properties']['enroll_text'] ) && isset( $request['enroll_text'] ) ) { + $prepared_item['enroll_text'] = $request['enroll_text']; + } + + // Post id. + if ( ! empty( $schema['properties']['post_id'] ) && isset( $request['post_id'] ) ) { + $prepared_item['product_id'] = $request['post_id']; + } + + // SKU. + if ( ! empty( $schema['properties']['sku'] ) && isset( $request['sku'] ) ) { + $prepared_item['sku'] = $request['sku']; + } + + /** + * Filters the access plan data before inserting in the db + * + * @since 1.0.0-beta.18 + * + * @param array $prepared_item Array of access plan item properties prepared for database. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + */ + $prepared_item = apply_filters( 'llms_rest_pre_insert_access_plan', $prepared_item, $request, $schema ); + + return $prepared_item; + } + + /** + * Updates an existing single LLMS_Access_Plan in the database + * + * This method should be used for access plan properties that require the access plan id in order to be saved in the database. + * + * @since 1.0.0-beta.18 + * + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + * @param array $prepared_item Array. + * @param bool $creating Optional. Whether we're in creation or update phase. Default true (create). + * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update. + */ + protected function update_additional_object_fields( $access_plan, $request, $schema, $prepared_item, $creating = true ) { + + $error = new WP_Error(); + + // Will contain the properties to set. + $to_set = array(); + + // Access expiration. + if ( ! empty( $schema['properties']['access_expiration'] ) && isset( $request['access_expiration'] ) ) { + $to_set['access_expiration'] = $request['access_expiration']; + } + + // Access expires. + if ( ! empty( $schema['properties']['access_expires'] ) && isset( $request['access_expires'] ) ) { + $access_expires = rest_parse_date( $request['access_expires'] ); + $to_set['access_expires'] = empty( $access_expires ) ? '' : date_i18n( 'Y-m-d H:i:s', $access_expires ); + } + + // Access length. + if ( ! empty( $schema['properties']['access_length'] ) && isset( $request['access_length'] ) ) { + $to_set['access_length'] = $request['access_length']; + } + + // Access period. + if ( ! empty( $schema['properties']['access_period'] ) && isset( $request['access_period'] ) ) { + $to_set['access_period'] = $request['access_period']; + } + + // Redirect. + if ( ! empty( $schema['properties']['redirect_type'] ) && isset( $request['redirect_type'] ) ) { + $to_set['checkout_redirect_type'] = $request['redirect_type']; + } + + // Redirect page. + if ( ! empty( $schema['properties']['redirect_page'] ) && isset( $request['redirect_page'] ) ) { + $redirect_page = get_post( $request['redirect_page'] ); + if ( $redirect_page && is_a( $redirect_page, 'WP_Post' ) ) { + $to_set['checkout_redirect_page'] = $request['redirect_page']; // maybe allow only published pages? + } + } + + // Redirect url. + if ( ! empty( $schema['properties']['redirect_url'] ) && isset( $request['redirect_url'] ) ) { + $to_set['checkout_redirect_url'] = $request['redirect_url']; + } + + // Price. + if ( ! empty( $schema['properties']['price'] ) && isset( $request['price'] ) ) { + $to_set['price'] = $request['price']; + } + + // Sale enabled. + if ( ! empty( $schema['properties']['sale_enabled'] ) && isset( $request['sale_enabled'] ) ) { + $to_set['on_sale'] = $request['sale_enabled'] ? 'yes' : 'no'; + } + + // Sale dates. + if ( ! empty( $schema['properties']['sale_date_start'] ) && isset( $request['sale_date_start'] ) ) { + $sale_date_start = rest_parse_date( $request['sale_date_start'] ); + $to_set['sale_start'] = empty( $sale_date_start ) ? '' : date_i18n( 'Y-m-d H:i:s', $sale_date_start ); + } + + if ( ! empty( $schema['properties']['sale_date_end'] ) && isset( $request['sale_date_end'] ) ) { + $sale_date_end = rest_parse_date( $request['sale_date_end'] ); + $to_set['sale_end'] = empty( $sale_date_end ) ? '' : date_i18n( 'Y-m-d H:i:s', $sale_date_end ); + } + // Sale price. + if ( ! empty( $schema['properties']['sale_price'] ) && isset( $request['sale_price'] ) ) { + $to_set['sale_price'] = $request['sale_price']; + } + + // Trial enabled. + if ( ! empty( $schema['properties']['trial_enabled'] ) && isset( $request['trial_enabled'] ) ) { + $to_set['trial_offer'] = $request['trial_enabled'] ? 'yes' : 'no'; + } + + // Trial Length. + if ( ! empty( $schema['properties']['trial_length'] ) && isset( $request['trial_length'] ) ) { + $to_set['trial_length'] = $request['trial_length']; + } + // Trial Period. + if ( ! empty( $schema['properties']['trial_period'] ) && isset( $request['trial_period'] ) ) { + $to_set['trial_period'] = $request['trial_period']; + } + // Trial price. + if ( ! empty( $schema['properties']['trial_price'] ) && isset( $request['trial_price'] ) ) { + $to_set['trial_price'] = $request['trial_price']; + } + + // Availability restrictions. + if ( ! empty( $schema['properties']['availability_restrictions'] ) && isset( $request['availability_restrictions'] ) ) { + $to_set['availability_restrictions'] = $request['availability_restrictions']; + } + + // Redirect forced. + if ( ! empty( $schema['properties']['redirect_forced'] ) && isset( $request['redirect_forced'] ) ) { + $to_set['checkout_redirect_forced'] = $request['redirect_forced']; + } + + // Frequency. + if ( ! empty( $schema['properties']['frequency'] ) && isset( $request['frequency'] ) ) { + $to_set['frequency'] = $request['frequency']; + } + + // Length. + if ( ! empty( $schema['properties']['length'] ) && isset( $request['length'] ) ) { + $to_set['length'] = $request['length']; + } + // Period. + if ( ! empty( $schema['properties']['period'] ) && isset( $request['period'] ) ) { + $to_set['period'] = $request['period']; + } + + $this->handle_props_interdependency( $to_set, $access_plan, $creating ); + + // Visibility. + if ( ! empty( $schema['properties']['visibiliy'] ) && isset( $request['visibility'] ) ) { + $visibility = $access_plan->set_visibility( $request['visibility'] ); + if ( is_wp_error( $visibility ) ) { + return $visibility; + } + } + + // Set bulk. + if ( ! empty( $to_set ) ) { + $update = $access_plan->set_bulk( $to_set, true ); + if ( is_wp_error( $update ) ) { + $error = $update; + } + } + + if ( $error->errors ) { + return $error; + } + + return ! empty( $to_set ) || ! empty( $visibility ); + } + + /** + * Handle properties interdependency + * + * @since 1.0.0-beta.18 + * + * @param array $to_set Array of properties to be set. + * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance. + * @param bool $creating Whether we're in creation or update phase. + * @return void + */ + private function handle_props_interdependency( &$to_set, $access_plan, $creating ) { + + // Access Plan properties as saved in the db. + $saved_props = $access_plan->toArray(); + + $this->add_subordinate_props( $to_set, $saved_props, $creating ); + + $this->unset_subordinate_props( $to_set, $saved_props ); + + } + + /** + * Add all the properties which need to be set as consequence of another setting + * + * These properties must be compared to the saved value before updating, because if equal they will produce an error(see update_post_meta()). + * + * @since 1.0.0-beta.18 + * + * @param array $to_set Array of properties to be set. + * @param array $saved_props Array of LLMS_Access_Plan properties as saved in the db. + * @param bool $creating Whether we're in creation or update phase. + * @return void + */ + private function add_subordinate_props( &$to_set, $saved_props, $creating ) { + + $subordinate_props = array(); + + // Merge new properties to set and saved props. + $props = wp_parse_args( $to_set, $saved_props ); + + // Paid plan. + if ( $props['price'] > 0 ) { + + $subordinate_props['is_free'] = 'no'; + + // One-time (no trial). + if ( 0 === $props['frequency'] ) { + $subordinate_props['trial_offer'] = 'no'; + } + } else { + + $subordinate_props['is_free'] = 'yes'; + $subordinate_props['price'] = 0; + $subordinate_props['frequency'] = 0; + $subordinate_props['on_sale'] = 'no'; + $subordinate_props['trial_offer'] = 'no'; + + } + + if ( ! $creating ) { // Remove already set properties. + + foreach ( $subordinate_props as $_prop => $value ) { + if ( isset( $saved_props[ $_prop ] ) && $saved_props[ $_prop ] === $value ) { + unset( $subordinate_props[ $_prop ] ); + } + } + } + + $to_set = array_merge( $to_set, $subordinate_props ); + + } + + /** + * Remove all the properties that do not need to be set, based on other properties + * + * @since 1.0.0-beta.18 + * + * @param array $to_set Array of properties to be set. + * @param array $saved_props Array of LLMS_Access_Plan properties as saved in the db. + * @return void + */ + private function unset_subordinate_props( &$to_set, $saved_props ) { + + // Merge new properties to set and saved props. + $props = wp_parse_args( $to_set, $saved_props ); + + // No need to create/update recurring props when it's a 1-time payment. + if ( 0 === $props['frequency'] ) { + unset( $to_set['length'], $to_set['period'] ); + } + + // No need to create/update trial props when no trial enabled. + if ( ! llms_parse_bool( $props['trial_offer'] ) ) { + unset( $to_set['trial_price'], $to_set['trial_length'], $to_set['trial_period'] ); + } + + // No need to create/update sale props when not on sale. + if ( ! llms_parse_bool( $props['on_sale'] ) ) { + unset( $to_set['sale_price'], $to_set['sale_end'], $to_set['sale_start'] ); + } + + // Unset redirect props based on redirect settings. + if ( 'url' === $props['checkout_redirect_type'] ) { + unset( $to_set['checkout_redirect_page'] ); + } elseif ( 'page' === $props['checkout_redirect_type'] ) { + unset( $to_set['checkout_redirect_url'] ); + } else { + unset( $to_set['checkout_redirect_url'], $to_set['checkout_redirect_page'] ); + } + + // Unset expiration props based on expiration settings. + if ( 'lifetime' === $props['access_expiration'] ) { + unset( $to_set['access_expires'], $to_set['access_length'], $to_set['access_period'] ); + } elseif ( 'limited-date' === $props['access_expiration'] ) { + unset( $to_set['access_length'], $to_set['access_period'] ); + } elseif ( 'limited-period' === $props['access_expiration'] ) { + unset( $to_set['access_expires'] ); + } + } + + /** + * Check if the current user, who has no permissions to manipulate the access plan post, can edit its related product. + * + * @since 1.0.0-beta.18 + * @since 1.0.0-beta.20 Made sure either we're creating or updating prior to check related product's permissions. + * + * @param boolean|WP_Error $has_permissions Whether or not the current user has the permission to manipulate the resource. + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + private function related_product_permissions_check( $has_permissions, $request ) { + + if ( llms_rest_is_authorization_required_error( $has_permissions ) ) { + + // `id` required on "reading/updating", `post_id` required on "creating". + if ( empty( $request['id'] ) && empty( $request['post_id'] ) ) { + return $has_permissions; + } + + $product_id = isset( $request['id'] ) /* not creation */ ? $this->get_object( (int) $request['id'] )->get( 'product_id' ) : (int) $request['post_id']; + + $product_post_type_object = get_post_type_object( get_post_type( $product_id ) ); + + if ( current_user_can( $product_post_type_object->cap->edit_post, $product_id ) ) { + $has_permissions = true; + } + } + + return $has_permissions; + } + + /** + * Allow request when the access plan limit per product is not reached. + * + * @since 1.0.0-beta.20 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error + */ + private function allow_request_when_access_plan_limit_not_reached( $request ) { + + // `id` required on "reading/updating", `post_id` required on "creating". + if ( empty( $request['id'] ) && empty( $request['post_id'] ) ) { + return true; + } + + $product_id = isset( $request['post_id'] ) ? $request['post_id'] : $this->get_object( (int) $request['id'] )->get( 'product_id' ); + $product = new LLMS_Product( $product_id ); + $limit = $product->get_access_plan_limit(); + + if ( count( $product->get_access_plans( false, false ) ) >= $limit ) { + + return llms_rest_bad_request_error( + sprintf( + // Translators: %1$d = access plans limit per product, %2$s access plan post type plural name, %3$s product post type singular name. + __( 'Only %1$d %2$s allowed per %3$s', 'lifterlms' ), + $limit, + strtolower( get_post_type_object( $this->post_type )->labels->name ), + strtolower( get_post_type_object( get_post_type( $product_id ) )->labels->singular_name ) + ) + ); + + } + + return true; + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-api-keys-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-api-keys-controller.php new file mode 100644 index 0000000000..a67314dfb5 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-api-keys-controller.php @@ -0,0 +1,512 @@ +<?php +/** + * REST Controller for API Keys. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_API_Keys_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added: `get_objects_from_query()`, `get_objects_query()`, `get_pagination_data_from_query()`, `prepare_collection_items_for_response()` methods overrides. + * `get_items()` method abstracted and moved in LLMS_REST_Controller. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_API_Keys_Controller extends LLMS_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'api-keys'; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'description', + 'last_access', + ); + + /** + * Check if the authenticated user can perform the request action. + * + * @since 1.0.0-beta.1 + * + * @return boolean + */ + protected function check_permissions() { + return current_user_can( 'manage_lifterlms_api_keys' ) ? true : llms_rest_authorization_required_error(); + } + + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Check if a given request has access to delete an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $params = parent::get_collection_params(); + + $params['permissions'] = array( + 'description' => __( 'Include only API keys matching a specific permission.', 'lifterlms' ), + 'type' => 'string', + 'enum' => array_keys( LLMS_REST_API()->keys()->get_permissions() ), + ); + + $params['user'] = array( + 'description' => __( 'Include only keys for the specified user(s). Accepts a single id or a comma separated list of ids.', 'lifterlms' ), + 'type' => 'string', + ); + + $params['user_not_in'] = array( + 'description' => __( 'Exclude keys for the specified user(s). Accepts a single id or a comma separated list of ids.', 'lifterlms' ), + 'type' => 'string', + ); + + return $params; + + } + + /** + * Get the API Key's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_item_schema() { + + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'api_key', + 'type' => 'object', + 'properties' => array( + 'description' => array( + 'description' => __( 'Friendly, human-readable name or description.', 'lifterlms' ), + 'type' => 'string', + 'required' => true, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'permissions' => array( + 'description' => __( 'Determines the capabilities and permissions of the key.', 'lifterlms' ), + 'type' => 'string', + 'required' => true, + 'context' => array( 'view', 'edit' ), + 'enum' => array_keys( LLMS_REST_API()->keys()->get_permissions() ), + ), + 'user_id' => array( + 'description' => __( 'The WordPress User ID of the key owner.', 'lifterlms' ), + 'type' => 'integer', + 'required' => true, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + 'validate_callback' => array( $this, 'validate_user_exists' ), + ), + ), + 'truncated_key' => array( + 'description' => __( 'The last 7 characters of the Consumer Key.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'last_access' => array( + 'description' => __( 'The date the key was last used. Format: Y-m-d H:i:s.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + } + + /** + * Check if a given request has access to read an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Check if a given request has access to read items. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Retrieve An API Key object by ID. + * + * @since 1.0.0-beta.1 + * + * @param int $id API Key ID. + * @param bool $hydrate If true, pulls all key data from the database on instantiation. + * @return WP_Error|LLMS_REST_API_Key + */ + protected function get_object( $id, $hydrate = true ) { + + $key = LLMS_REST_API()->keys()->get( $id, $hydrate ); + return $key ? $key : llms_rest_not_found_error(); + + } + + /** + * Create an API Key + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + + $prepared = $this->prepare_item_for_database( $request ); + $key = LLMS_REST_API()->keys()->create( $prepared ); + if ( is_wp_error( $request ) ) { + $request->add_data( array( 'status' => 400 ) ); + return $request; + } + + $response = $this->prepare_item_for_response( $key, $request ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $key->get( 'id' ) ) ) ); + + return $response; + + } + + /** + * Delete API Key + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + + $key = $this->get_object( $request['id'], false ); + if ( ! is_wp_error( $key ) ) { + $key->delete(); + } + + $response = rest_ensure_response( null ); + $response->set_status( 204 ); + + return $response; + + } + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request. + * + * @since 1.0.0-beta.7 + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Full details about the request. + * @return LLMS_REST_API_Keys_Query + */ + protected function get_objects_query( $prepared, $request ) { + + return new LLMS_REST_API_Keys_Query( $prepared ); + + } + + /** + * Retrieve an array of objects from the result of $this->get_objects_query(). + * + * @since 1.0.0-beta.7 + * + * @param WP_Query $query Query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + return $query->get_keys(); + + } + + /** + * Retrieve pagination information from an objects query. + * + * @since 1.0.0-beta.7 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + $total_results = (int) $query->found_results; + $current_page = isset( $prepared['page'] ) ? (int) $prepared['page'] : 1; + $total_pages = (int) $query->max_pages; + + return compact( 'current_page', 'total_results', 'total_pages' ); + + } + + /** + * Prepare collection items for response. + * + * @since 1.0.0-beta.7 + * + * @param array $objects Array of objects to be prepared for response. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + + foreach ( $objects as $object ) { + $item = $this->prepare_item_for_response( $object, $request ); + if ( ! is_wp_error( $item ) ) { + $items[] = $this->prepare_response_for_collection( $item ); + } + } + + return $items; + } + + /** + * Update an API Key + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + + $prepared = $this->prepare_item_for_database( $request ); + $key = LLMS_REST_API()->keys()->update( $prepared ); + if ( is_wp_error( $request ) ) { + $request->add_data( array( 'status' => 400 ) ); + return $request; + } + + $response = $this->prepare_item_for_response( $key, $request ); + + return $response; + + } + + /** + * Format query arguments from a collection GET request to be passed to a LLMS_REST_API_Keys_Query + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_collection_query_args( $request ) { + + $args = array(); + $params = $this->get_collection_params(); + + foreach ( array_keys( $params ) as $param ) { + + if ( ! isset( $request[ $param ] ) || in_array( $param, array( 'order', 'orderby' ), true ) ) { + continue; + } + + $args[ $param ] = $request[ $param ]; + + if ( in_array( $param, array( 'include', 'exclude', 'user', 'user_not_in' ), true ) ) { + $args[ $param ] = array_map( 'absint', explode( ',', $args[ $param ] ) ); + } + } + + if ( isset( $request['orderby'] ) || isset( $request['order'] ) ) { + $orderby = isset( $request['orderby'] ) ? $request['orderby'] : $params['orderby']['default']; + $order = isset( $request['order'] ) ? $request['order'] : $params['order']['default']; + $args['sort'] = array( $orderby => $order ); + } + + return $args; + + } + + /** + * Prepare API Key for insert/update + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|array + */ + protected function prepare_item_for_database( $request ) { + + $prepared = array(); + + if ( isset( $request['id'] ) ) { + $existing = $this->get_object( $request['id'] ); + if ( is_wp_error( $existing ) ) { + return $existing; + } + $prepared['id'] = $existing->get( 'id' ); + } + + $schema = $this->get_item_schema(); + + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $prepared['description'] = $request['description']; + } + + if ( ! empty( $schema['properties']['user_id'] ) && isset( $request['user_id'] ) ) { + $prepared['user_id'] = (int) $request['user_id']; + } + + if ( ! empty( $schema['properties']['permissions'] ) && isset( $request['permissions'] ) ) { + $prepared['permissions'] = $request['permissions']; + } + + return $prepared; + + } + + /** + * Prepare an API Key for a REST response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Pass the `$request` parameter to `prepare_links()`. + * + * @param LLMS_REST_API_Key $item API Key object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $item, $request ) { + + $data = array( + 'id' => $item->get( 'id' ), + ); + + // Add all readable properties. + foreach ( $this->get_fields_for_response( $request ) as $field ) { + $data[ $field ] = $item->get( $field ); + } + + // Is a creation request, return consumer key & secret. + if ( 'POST' === $request->get_method() && sprintf( '/%1$s/%2$s', $this->namespace, $this->rest_base ) === $request->get_route() ) { + $data['consumer_key'] = $item->get( 'consumer_key_one_time' ); + $data['consumer_secret'] = $item->get( 'consumer_secret' ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + // Add links. + $response->add_links( $this->prepare_links( $item, $request ) ); + + return $response; + + } + + /** + * Prepare a `_links` object for an API Key. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added $request parameter. + * + * @param LLMS_REST_API_Key $item API Key object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $item, $request ) { + + $links = parent::prepare_links( $item, $request ); + $links['user'] = array( + 'href' => rest_url( sprintf( 'wp/v2/users/%d', $item->get( 'user_id' ) ) ), + ); + + return $links; + + } + + /** + * Check if a given request has access to update an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Validate submitted user IDs are real user ids. + * + * @since 1.0.0-beta.1 + * + * @param int $value User-submitted value. + * @return boolean + */ + public function validate_user_exists( $value ) { + + $user = get_user_by( 'id', $value ); + return $user ? true : false; + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-courses-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-courses-controller.php new file mode 100644 index 0000000000..5b5a4297d6 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-courses-controller.php @@ -0,0 +1,1249 @@ +<?php +/** + * REST Courses Controller + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.18 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Courses_Controller class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Make `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` nullable. + * Allow `prerequisite` and `prerequisite_track` to be cleared (set to 0). + * Also: + * - if `prerequisite` is not a valid course the course `prerequisite` will be set to 0; + * - if `prerequisite_track` is not a valid course track, the course `prerequisite_track` will be set to 0. + * + * `update_additional_object_fields()` returns false if nothing to update. + * Properties `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` handling + * moved here from `prepare_item_for_database()` method to `update_additional_object_fields()` method so to better handle the update of the + * course's properties `time_period` and `enrollment_period`. + * Added logic to prevent trying to update "derived only" courses's properties (`time_period`, `enrollment_period`, `has_prerequisite`) + * if their values didn't really change, otherwise we'd get a WP_Error which the consumer cannot avoid having no direct control on those properties. + * 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. + * Overridden `get_object_id()` method to avoid using the deprecated `LLMS_Course::get_id()` which, + * as coded in the `LLMS_REST_Controller_Stubs::get_object_id()` takes precedence over `get( 'id' )`. + * @since 1.0.0-beta.8 Fixed `sales_page_type` not returned as `none` if course's `sales_page_content_type` property is empty. + * Renamed `sales_page_page_type` and `sales_page_page_url` properties, respectively to `sales_page_type` and `sales_page_url` according to the specs. + * Add missing quotes in enrollment/access default messages shortcodes. + * Call `set_bulk()` llms post method passing `true` as second parameter, so to instruct it to return a WP_Error on failure. + * Add missing quotes in enrollment/access default messages shortcodes. + * `sales_page_page_id` and `sales_page_url` always returned in edit context. + * @since 1.0.0-beta.9 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. + * Also made sure course's `instructor` is at least set as the post author. + * Defined `instructors` validate callback so to make sure instructors list is either not empty and composed by real user ids. + * Fixed `sales_page_url` not returned in `edit` context. + * Removed `create_llms_post()` and `get_object()` methods, now abstracted in `LLMS_REST_Posts_Controller` class. + * `llms_rest_course_filters_removed_for_response` filter hook added. + * Added `llms_rest_course_item_schema`, `llms_rest_pre_insert_course`, `llms_rest_prepare_course_object_response`, `llms_rest_course_links` filter hooks. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Courses_Controller extends LLMS_REST_Posts_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'courses'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'course'; + + /** + * Enrollments controller + * + * @var LLMS_REST_Enrollments_Controller + */ + protected $enrollments_controller; + + /** + * Sections controller + * + * @var LLMS_REST_Sections_Controller + */ + protected $sections_controller; + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + */ + public function __construct() { + + $this->enrollments_controller = new LLMS_REST_Enrollments_Controller(); + $this->enrollments_controller->set_collection_params( $this->get_enrollments_collection_params() ); + + $this->sections_controller = new LLMS_REST_Sections_Controller( '' ); + $this->sections_controller->set_collection_params( $this->get_course_content_collection_params() ); + + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function register_routes() { + + parent::register_routes(); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)/enrollments', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique Course Identifier. The WordPress Post ID', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this->enrollments_controller, 'get_items' ), + 'permission_callback' => array( $this->enrollments_controller, 'get_items_permissions_check' ), + 'args' => $this->enrollments_controller->get_collection_params(), + ), + 'schema' => array( $this->enrollments_controller, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)/content', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique Course Identifier. The WordPress Post ID', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_course_content_items' ), + 'permission_callback' => array( $this->sections_controller, 'get_items_permissions_check' ), + 'args' => $this->sections_controller->get_collection_params(), + ), + 'schema' => array( $this->sections_controller, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieve an ID from the object + * + * @since 1.0.0-beta.7 + * + * @param LLMS_Course $object LLMS_Course object. + * @return int + */ + protected function get_object_id( $object ) { + + // For example. + return $object->get( 'id' ); + + } + + /** + * Get the Course's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.8 Renamed `sales_page_page_type` and `sales_page_page_url` properties, respectively to `sales_page_type` and `sales_page_url` according to the specs. + * Add missing quotes in enrollment/access default messages shortcodes. + * @since 1.0.0-beta.9 Make sure instructors list is either not empty and composed by real user ids. + * Added `llms_rest_course_item_schema` filter hook. + * @return array + */ + public function get_item_schema() { + + $schema = parent::get_item_schema(); + + $course_properties = array( + 'catalog_visibility' => array( + 'description' => __( 'Visibility of the course in catalogs and search results.', 'lifterlms' ), + 'type' => 'string', + 'enum' => array_keys( llms_get_product_visibility_options() ), + 'default' => 'catalog_search', + 'context' => array( 'view', 'edit' ), + ), + // consider to move tags and cats in the posts controller abstract. + 'categories' => array( + 'description' => __( 'List of course categories.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'tags' => array( + 'description' => __( 'List of course tags.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'difficulties' => array( + 'description' => __( 'List of course difficulties.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'tracks' => array( + 'description' => __( 'List of course tracks.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'instructors' => array( + 'description' => __( 'List of course instructors. Defaults to current user when creating a new post.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'arg_options' => array( + 'validate_callback' => 'llms_validate_instructors', + ), + 'context' => array( 'view', 'edit' ), + ), + 'audio_embed' => array( + 'description' => __( 'URL to an oEmbed enable audio URL.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'video_embed' => array( + 'description' => __( 'URL to an oEmbed enable video URL.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'capacity_enabled' => array( + 'description' => __( 'Determines if an enrollment capacity limit is enabled.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + ), + 'capacity_limit' => array( + 'description' => __( 'Number of students who can be enrolled in the course before enrollment closes.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'capacity_message' => array( + 'description' => __( 'Message displayed when enrollment capacity has been reached.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'prerequisite' => array( + 'description' => __( 'Course ID of the prerequisite course.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'prerequisite_track' => array( + 'description' => __( 'Term ID of a prerequisite track.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'length' => array( + 'description' => __( 'User defined course length.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw length description.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered length description.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'restricted_message' => array( + 'description' => __( 'Message displayed when non-enrolled visitors try to access restricted course content (lessons, quizzes, etc..) directly.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'default' => __( 'You must enroll in this course to access course content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( 'You must enroll in this course to access course content.', 'lifterlms' ), + ), + 'access_closes_date' => array( + 'description' => __( + 'Date when the course closes. After this date enrolled students may no longer view and interact with the restricted course content. + If blank the course is open indefinitely after the the access_opens_date has passed. + Does not affect course enrollment, see enrollment_opens_date to control the course enrollment close date. + Format: Y-m-d H:i:s.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'access_closes_message' => array( + 'description' => __( 'Message displayed to enrolled students when the course is accessed after the access_closes_date has passed.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( 'This course closed on [lifterlms_course_info id="{{course_id}}" key="end_date"].', 'lifterlms' ), + ), + 'access_opens_date' => array( + 'description' => __( + 'Date when the course opens, allowing enrolled students to begin to view and interact with the restricted course content. + If blank the course is open until after the access_closes_date has passed. + Does not affect course enrollment, see enrollment_opens_date to control the course enrollment start date. + Format: Y-m-d H:i:s.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'access_opens_message' => array( + 'description' => __( 'Message displayed to enrolled students when the course is accessed before the access_opens_date has passed.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( 'This course opens on [lifterlms_course_info id="{{course_id}}" key="start_date"].', 'lifterlms' ), + ), + 'enrollment_closes_date' => array( + 'description' => __( + 'Date when the course enrollment closes. + If blank course enrollment is open indefinitely after the the enrollment_opens_date has passed. + Does not affect course content access, see access_opens_date to control course access close date. + Format: Y-m-d H:i:s.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'enrollment_closes_message' => array( + 'description' => __( 'Message displayed to visitors when attempting to enroll into a course after the enrollment_closes_date has passed.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( 'Enrollment in this course closed on [lifterlms_course_info id="{{course_id}}" key="enrollment_end_date"].', 'lifterlms' ), + ), + 'enrollment_opens_date' => array( + 'description' => __( + 'Date when the course enrollment opens. + If blank course enrollment is open until after the enrollment_closes_date has passed. + Does not affect course content access, see access_opens_date to control course access start date. + Format: Y-m-d H:i:s.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'enrollment_opens_message' => array( + 'description' => __( 'Message displayed to visitors when attempting to enroll into a course before the enrollment_opens_date has passed.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( 'Enrollment in this course opens on [lifterlms_course_info id="{{course_id}}" key="enrollment_start_date"].', 'lifterlms' ), + ), + 'sales_page_page_id' => array( + 'description' => __( + 'The WordPress page ID of the sales page. Required when sales_page_type equals page. Only returned when the sales_page_type equals page.', + 'lifterlms' + ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'sales_page_type' => array( + 'description' => __( + 'Determines the type of sales page content to display.<br> - <code>none</code> displays the course content.<br> - <code>content</code> displays alternate content from the <code>excerpt</code> property.<br> - <code>page</code> redirects to the WordPress page defined in <code>content_page_id</code>.<br> - <code>url</code> redirects to the URL defined in <code>content_page_url</code>', + 'lifterlms' + ), + 'type' => 'string', + 'default' => 'none', + 'enum' => array_keys( llms_get_sales_page_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'sales_page_url' => array( + 'description' => __( + 'The URL of the sales page content. Required when <code>content_type</code> equals <code>url</code>. Only returned when the <code>content_type</code> equals <code>url</code>.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'video_tile' => array( + 'description' => __( 'When true the video_embed will be used on the course tiles (on the catalog, for example) instead of the featured image.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ); + + $schema['properties'] = array_merge( (array) $schema['properties'], $course_properties ); + + /** + * Filter item schema for the course controller. + * + * @since 1.0.0-beta.9 + * + * @param array $schema Item schema data. + */ + return apply_filters( 'llms_rest_course_item_schema', $schema ); + + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.8 Fixed `sales_page_type` not set as `none` if course's `sales_page_content_type` property is empty. + * Also Renamed `sales_page_page_type` and `sales_page_page_url` properties, respectively to `sales_page_type` and `sales_page_url` according to the specs. + * Always return `sales_page_url` and `sales_page_page_id` when in `edit` context. + * @since 1.0.0-beta.9 Fixed `sales_page_url` not returned in `edit` context. + * Added `llms_rest_prepare_course_object_response` filter hook. + * + * @param LLMS_Course $course Course object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $course, $request ) { + + $data = parent::prepare_object_for_response( $course, $request ); + + // Catalog visibility. + $data['catalog_visibility'] = $course->get_product()->get_catalog_visibility(); + + // Categories. + $data['categories'] = $course->get_categories( + array( + 'fields' => 'ids', + ) + ); + + // Tags. + $data['tags'] = $course->get_tags( + array( + 'fields' => 'ids', + ) + ); + + // Difficulties. + $difficulties = $course->get_difficulty( 'term_id' ); + $difficulties = empty( $difficulties ) ? array() : array( $difficulties ); + $data['difficulties'] = $difficulties; + + // Tracks. + $data['tracks'] = $course->get_tracks( + array( + 'fields' => 'ids', + ) + ); + + // Instructors. + $instructors = $course->get_instructors(); + $instructors = empty( $instructors ) ? array() : wp_list_pluck( $instructors, 'id' ); + $data['instructors'] = $instructors; + + // Audio Embed. + $data['audio_embed'] = $course->get( 'audio_embed' ); + + // Video Embed. + $data['video_embed'] = $course->get( 'video_embed' ); + + // Video tile. + $data['video_tile'] = 'yes' === $course->get( 'tile_featured_video' ); + + // Capacity. + $data['capacity_enabled'] = 'yes' === $course->get( 'enable_capacity' ); + + $data['capacity_limit'] = $course->get( 'capacity' ); + $data['capacity_message'] = array( + 'raw' => $course->get( 'capacity_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'capacity_message' ) ), + ); + + // Prerequisite. + $data['prerequisite'] = (int) $course->get_prerequisite_id(); + + // Prerequisite track. + $data['prerequisite_track'] = (int) $course->get_prerequisite_id( 'course_track' ); + + // Length. + $data['length'] = array( + 'raw' => $course->get( 'length', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'length' ) ), + ); + + // Restricted message. + $data['restricted_message'] = array( + 'raw' => $course->get( 'content_restricted_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'content_restricted_message' ) ), + ); + + // Access open/closed. + $data['access_opens_date'] = $course->get_date( 'start_date', 'Y-m-d H:i:s' ); + $data['access_closes_date'] = $course->get_date( 'end_date', 'Y-m-d H:i:s' ); + + $data['access_opens_message'] = array( + 'raw' => $course->get( 'course_opens_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'course_opens_message' ) ), + ); + + $data['access_closes_message'] = array( + 'raw' => $course->get( 'course_closed_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'course_closed_message' ) ), + ); + + // Enrollment open/closed. + $data['enrollment_opens_date'] = $course->get_date( 'enrollment_start_date', 'Y-m-d H:i:s' ); + $data['enrollment_closes_date'] = $course->get_date( 'enrollment_end_date', 'Y-m-d H:i:s' ); + + $data['enrollment_opens_message'] = array( + 'raw' => $course->get( 'enrollment_opens_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'enrollment_opens_message' ) ), + ); + + $data['enrollment_closes_message'] = array( + 'raw' => $course->get( 'enrollment_closed_message', $raw = true ), + 'rendered' => do_shortcode( $course->get( 'enrollment_closed_message' ) ), + ); + + // Sales page page type. + $data['sales_page_type'] = $course->get( 'sales_page_content_type' ); + $data['sales_page_type'] = $data['sales_page_type'] ? $data['sales_page_type'] : 'none'; + + // Sales page id. + if ( 'page' === $data['sales_page_type'] || 'edit' === $request['context'] ) { + $data['sales_page_page_id'] = $course->get( 'sales_page_content_page_id' ); + } + + // Sales page url. + if ( 'url' === $data['sales_page_type'] || 'edit' === $request['context'] ) { + $data['sales_page_url'] = $course->get( 'sales_page_content_url' ); + } + + /** + * Filters the course data for a response. + * + * @since 1.0.0-beta.9 + * + * @param array $data Array of course properties prepared for response. + * @param LLMS_Course $course Course object. + * @param WP_REST_Request $request Full details about the request. + */ + return apply_filters( 'llms_rest_prepare_course_object_response', $data, $course, $request ); + + } + + /** + * Prepares a single post for create or update. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` + * treated in @see `update_additional_object_fields()` method so to better handle the update of the + * course's properties `time_period` and `enrollment_period` whose values are derived from them and need to be + * passed to `$course->set_bulk()` only if they differ from their current values, otherwise we'd get a WP_Error + * which the consumer cannot avoid having no direct control on those properties. + * Made `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` nullable. + * @since 1.0.0-beta.8 Renamed `sales_page_page_type` and `sales_page_page_url` properties, respectively to `sales_page_type` and `sales_page_url` according to the specs. + * @since 1.0.0-beta.9 Added `llms_rest_pre_insert_course` filter hook. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of llms post args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + + $prepared_item = parent::prepare_item_for_database( $request ); + $schema = $this->get_item_schema(); + + // Course Audio embed URL. + if ( ! empty( $schema['properties']['audio_embed'] ) && isset( $request['audio_embed'] ) ) { + $prepared_item['audio_embed'] = $request['audio_embed']; + } + + // Course Video embed URL. + if ( ! empty( $schema['properties']['video_embed'] ) && isset( $request['video_embed'] ) ) { + $prepared_item['video_embed'] = $request['video_embed']; + } + + // Video tile. + if ( ! empty( $schema['properties']['video_tile'] ) && isset( $request['video_tile'] ) ) { + $prepared_item['tile_featured_video'] = empty( $request['video_tile'] ) ? 'no' : 'yes'; + } + + // Capacity enabled. + if ( ! empty( $schema['properties']['capacity_enabled'] ) && isset( $request['capacity_enabled'] ) ) { + $prepared_item['enable_capacity'] = empty( $request['capacity_enabled'] ) ? 'no' : 'yes'; + } + + // Capacity message. + if ( ! empty( $schema['properties']['capacity_message'] ) && isset( $request['capacity_message'] ) ) { + if ( is_string( $request['capacity_message'] ) ) { + $prepared_item['capacity_message'] = $request['capacity_message']; + } elseif ( isset( $request['capacity_message']['raw'] ) ) { + $prepared_item['capacity_message'] = $request['capacity_message']['raw']; + } + } + + // Capacity limit. + if ( ! empty( $schema['properties']['capacity_limit'] ) && isset( $request['capacity_limit'] ) ) { + $prepared_item['capacity'] = $request['capacity_limit']; + } + + // Restricted message. + if ( ! empty( $schema['properties']['restricted_message'] ) && isset( $request['restricted_message'] ) ) { + if ( is_string( $request['restricted_message'] ) ) { + $prepared_item['content_restricted_message'] = $request['restricted_message']; + } elseif ( isset( $request['restricted_message']['raw'] ) ) { + $prepared_item['content_restricted_message'] = $request['restricted_message']['raw']; + } + } + + // Length. + if ( ! empty( $schema['properties']['length'] ) && isset( $request['length'] ) ) { + if ( is_string( $request['length'] ) ) { + $prepared_item['length'] = $request['length']; + } elseif ( isset( $request['length']['raw'] ) ) { + $prepared_item['length'] = $request['length']['raw']; + } + } + + // Sales page. + if ( ! empty( $schema['properties']['sales_page_type'] ) && isset( $request['sales_page_type'] ) ) { + $prepared_item['sales_page_content_type'] = $request['sales_page_type']; + } + + if ( ! empty( $schema['properties']['sales_page_page_id'] ) && isset( $request['sales_page_page_id'] ) ) { + $sales_page = get_post( $request['sales_page_page_id'] ); + if ( $sales_page && is_a( $sales_page, 'WP_Post' ) ) { + $prepared_item['sales_page_content_page_id'] = $request['sales_page_page_id']; // maybe allow only published pages? + } else { + $prepared_item['sales_page_content_page_id'] = 0; + } + } + + if ( ! empty( $schema['properties']['sales_page_url'] ) && isset( $request['sales_page_url'] ) ) { + $prepared_item['sales_page_content_url'] = $request['sales_page_url']; + } + + /** + * Filters the course data for a response. + * + * @since 1.0.0-beta.9 + * + * @param array $prepared_item Array of course item properties prepared for database. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + */ + return apply_filters( 'llms_rest_pre_insert_course', $prepared_item, $request, $schema ); + + } + + /** + * Updates a single llms course. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Allow `prerequisite` and `prerequisite_track` to be cleared (set to 0). + * Also: + * - if `prerequisite` is not a valid course the course `prerequisite` will be set to 0; + * - if `prerequisite_track` is not a valid course track, the course `prerequisite_track` will be set to 0. + * + * Return false if nothing to update. + * + * Properties `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` handling + * moved here from `prepare_item_for_database()` method so to better handle the update of the + * course's properties `time_period` and `enrollment_period`. + * + * Made `access_opens_date`, `access_closes_date`, `enrollment_opens_date`, `enrollment_closes_date` properties nullable. + * + * Added logic to prevent trying to update "derived only" courses's properties (`time_period`, `enrollment_period`, `has_prerequisite`) + * if their values didn't really change, otherwise we'd get a WP_Error which the consumer cannot avoid having no direct control on those properties. + * @since 1.0.0-beta.8 Call `set_bulk()` llms post method passing `true` as second parameter, + * so to instruct it to return a WP_Error on failure. + * @since 1.0.0-beta.9 Use `WP_Error::$errors` in place of `WP_Error::has_errors()` to support WordPress version prior to 5.1. + * Also made sure course's `instructor` is at least set as the post author. + * + * @param LLMS_Course $course LLMS_Course instance. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + * @param array $prepared_item Array. + * @param bool $creating Optional. Whether we're in creation or update phase. Default true (create). + * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update. + */ + protected function update_additional_object_fields( $course, $request, $schema, $prepared_item, $creating = true ) { + + $error = new WP_Error(); + + // Course catalog visibility. + if ( ! empty( $schema['properties']['catalog_visibility'] ) && isset( $request['catalog_visibility'] ) ) { + $course->get_product()->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Instructors. + if ( ! empty( $schema['properties']['instructors'] ) ) { + + $instructors = array(); + + if ( isset( $request['instructors'] ) ) { + foreach ( $request['instructors'] as $instructor_id ) { + $user_data = get_userdata( $instructor_id ); + if ( ! empty( $user_data ) ) { + $instructors[] = array( + 'id' => $instructor_id, + 'name' => $user_data->display_name, + ); + } + } + } + + // When creating always make sure the instructors are set. + // Note: `$course->set_instructor( $instructors )` when `$instructors` is empty + // will set the course's author as course's instructor. + if ( $creating || ( ! $creating && isset( $request['instructors'] ) ) ) { + $course->set_instructors( $instructors ); + } + } + + $to_set = array(); + + // Access dates. + if ( ! empty( $schema['properties']['access_opens_date'] ) && isset( $request['access_opens_date'] ) ) { + $access_opens_date = rest_parse_date( $request['access_opens_date'] ); + $to_set['start_date'] = empty( $access_opens_date ) ? '' : date_i18n( 'Y-m-d H:i:s', $access_opens_date ); + } + + if ( ! empty( $schema['properties']['access_closes_date'] ) && isset( $request['access_closes_date'] ) ) { + $access_closes_date = rest_parse_date( $request['access_closes_date'] ); + $to_set['end_date'] = empty( $access_closes_date ) ? '' : date_i18n( 'Y-m-d H:i:s', $access_closes_date ); + } + + // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + if ( ! empty( $to_set['start_date'] ) || ! empty( $to_set['end_date'] ) ) { + $to_set['time_period'] = 'yes'; + } else { + $to_set['time_period'] = 'no'; + } + + // Enrollment dates. + if ( ! empty( $schema['properties']['enrollment_opens_date'] ) && isset( $request['enrollment_opens_date'] ) ) { + $enrollment_opens_date = rest_parse_date( $request['enrollment_opens_date'] ); + $to_set['enrollment_start_date'] = empty( $enrollment_opens_date ) ? '' : date_i18n( 'Y-m-d H:i:s', $enrollment_opens_date ); + } + + if ( ! empty( $schema['properties']['enrollment_closes_date'] ) && isset( $request['enrollment_closes_date'] ) ) { + $enrollment_closes_date = rest_parse_date( $request['enrollment_closes_date'] ); + $to_set['enrollment_end_date'] = empty( $enrollment_closes_date ) ? '' : date_i18n( 'Y-m-d H:i:s', $enrollment_closes_date ); + } + + // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + if ( ! empty( $to_set['enrollment_start_date'] ) || ! empty( $to_set['enrollment_end_date'] ) ) { + $to_set['enrollment_period'] = 'yes'; + } else { + $to_set['enrollment_period'] = 'no'; + } + + // Prerequisite. + if ( ! empty( $schema['properties']['prerequisite'] ) && isset( $request['prerequisite'] ) ) { + // check if course exists. + $prerequisite = llms_get_post( $request['prerequisite'] ); + if ( is_a( $prerequisite, 'LLMS_Course' ) ) { + $to_set['prerequisite'] = $request['prerequisite']; + } else { + $to_set['prerequisite'] = 0; + } + } + + // Prerequisite track. + if ( ! empty( $schema['properties']['prerequisite_track'] ) && isset( $request['prerequisite_track'] ) ) { + // check if the track exists. + $track = new LLMS_Track( $request['prerequisite_track'] ); + if ( $track->term ) { + $to_set['prerequisite_track'] = $request['prerequisite_track']; + } else { + $to_set['prerequisite_track'] = 0; + } + } + + // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + if ( ! empty( $to_set['prerequisite'] ) || ! empty( $to_set['prerequisite_track'] ) ) { + $to_set['has_prerequisite'] = 'yes'; + } else { + $to_set['has_prerequisite'] = 'no'; + } + + /** + * The following properties have a default value that contains a placeholder ({{course_id}}) that can be "expanded" only + * after the course has been created. + */ + // Access opens/closes messages. + if ( ! empty( $schema['properties']['access_opens_message'] ) && isset( $request['access_opens_message'] ) ) { + if ( is_string( $request['access_opens_message'] ) ) { + $to_set['course_opens_message'] = $request['access_opens_message']; + } elseif ( isset( $request['access_opens_message']['raw'] ) ) { + $to_set['course_opens_message'] = $request['access_opens_message']['raw']; + } + } + + if ( ! empty( $schema['properties']['access_closes_message'] ) && isset( $request['access_closes_message'] ) ) { + if ( is_string( $request['access_closes_message'] ) ) { + $to_set['course_closed_message'] = $request['access_closes_message']; + } elseif ( isset( $request['access_closes_message']['raw'] ) ) { + $to_set['course_closed_message'] = $request['access_closes_message']['raw']; + } + } + + // Enrolmments opens/closes messages. + if ( ! empty( $schema['properties']['enrollment_opens_message'] ) && isset( $request['enrollment_opens_message'] ) ) { + if ( is_string( $request['enrollment_opens_message'] ) ) { + $to_set['enrollment_opens_message'] = $request['enrollment_opens_message']; + } elseif ( isset( $request['enrollment_opens_message']['raw'] ) ) { + $to_set['enrollment_opens_message'] = $request['enrollment_opens_message']['raw']; + } + } + + if ( ! empty( $schema['properties']['enrollment_closes_message'] ) && isset( $request['enrollment_closes_message'] ) ) { + if ( is_string( $request['enrollment_closes_message'] ) ) { + $to_set['enrollment_closed_message'] = $request['enrollment_closes_message']; + } elseif ( isset( $request['enrollment_closes_message']['raw'] ) ) { + $to_set['enrollment_closed_message'] = $request['enrollment_closes_message']['raw']; + } + } + + // Are we creating a course? + // If so, replace the placeholder with the actual course id. + if ( $creating ) { + + $_to_expand_props = array( + 'course_opens_message', + 'course_closed_message', + 'enrollment_opens_message', + 'enrollment_closed_message', + ); + + $course_id = $course->get( 'id' ); + + foreach ( $_to_expand_props as $prop ) { + if ( ! empty( $to_set[ $prop ] ) ) { + $to_set[ $prop ] = str_replace( '{{course_id}}', $course_id, $to_set[ $prop ] ); + } + } + } else { // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + $_props = array( + 'time_period', + 'enrollment_period', + 'has_prerequisite', + ); + foreach ( $_props as $_prop ) { + if ( isset( $to_set[ $_prop ] ) && $to_set[ $_prop ] === $course->get( $_prop ) ) { + unset( $to_set[ $_prop ] ); + } + } + } + + // Set bulk. + if ( ! empty( $to_set ) ) { + $update = $course->set_bulk( $to_set, true ); + if ( is_wp_error( $update ) ) { + $error = $update; + } + } + + if ( $error->errors ) { + return $error; + } + + return ! empty( $to_set ); + + } + + /** + * Maps a taxonomy name to the relative rest base + * + * @since 1.0.0-beta.1 + * + * @param object $taxonomy The taxonomy object. + * @return string The taxonomy rest base. + */ + protected function get_taxonomy_rest_base( $taxonomy ) { + + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + $taxonomy_base_map = array( + 'course_cat' => 'categories', + 'course_difficulty' => 'difficulties', + 'course_tag' => 'tags', + 'course_track' => 'tracks', + ); + + return isset( $taxonomy_base_map[ $base ] ) ? $taxonomy_base_map[ $base ] : $base; + + } + + /** + * Get action/filters to be removed before preparing the item for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 `llms_rest_course_filters_removed_for_response` filter hook added. + * + * @param LLMS_Course $course Course object. + * @return array Array of action/filters to be removed for response. + */ + protected function get_filters_to_be_removed_for_response( $course ) { + + $filters = array(); + + if ( llms_blocks_is_post_migrated( $course->get( 'id' ) ) ) { + $filters = array( + // hook => [callback, priority]. + 'lifterlms_single_course_after_summary' => array( + // Course Information. + array( + 'callback' => 'lifterlms_template_single_meta_wrapper_start', + 'priority' => 5, + ), + array( + 'callback' => 'lifterlms_template_single_length', + 'priority' => 10, + ), + array( + 'callback' => 'lifterlms_template_single_difficulty', + 'priority' => 20, + ), + array( + 'callback' => 'lifterlms_template_single_course_tracks', + 'priority' => 25, + ), + array( + 'callback' => 'lifterlms_template_single_course_categories', + 'priority' => 30, + ), + array( + 'callback' => 'lifterlms_template_single_course_tags', + 'priority' => 35, + ), + array( + 'callback' => 'lifterlms_template_single_meta_wrapper_end', + 'priority' => 50, + ), + // Course Progress. + array( + 'callback' => 'lifterlms_template_single_course_progress', + 'priority' => 60, + ), + // Course Syllabus. + array( + 'callback' => 'lifterlms_template_single_syllabus', + 'priority' => 90, + ), + // Instructors. + array( + 'callback' => 'lifterlms_template_course_author', + 'priority' => 40, + ), + // Pricing Table. + array( + 'callback' => 'lifterlms_template_pricing_table', + 'priority' => 60, + ), + ), + ); + } + + /** + * Modify the array of filters to be removed before building the response. + * + * @since 1.0.0-beta.9 + * + * @param array $filters Array of filters to be removed. + * @param LLMS_Course $course Course object. + */ + return apply_filters( 'llms_rest_course_filters_removed_for_response', $filters, $course ); + + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.9 Added `llms_rest_course_links` filter hook. + * @since 1.0.0-beta.14 Added $request parameter. + * @since 1.0.0-beta.18 Fixed access plans link. + * + * @param LLMS_Course $course LLMS Course. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $course, $request ) { + + $links = parent::prepare_links( $course, $request ); + $course_id = $course->get( 'id' ); + + $course_links = array(); + + // Access plans. + $course_links['access_plans'] = array( + 'href' => add_query_arg( + 'post_id', + $course_id, + rest_url( sprintf( '%s/%s', 'llms/v1', 'access-plans' ) ) + ), + ); + + // Enrollments. + $course_links['enrollments'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d/%s', $this->namespace, $this->rest_base, $course_id, 'enrollments' ) ), + ); + + // Instructors. + $course_links['instructors'] = array( + 'href' => add_query_arg( + 'post', + $course_id, + rest_url( sprintf( '%s/%s', 'llms/v1', 'instructors' ) ) + ), + ); + + // Prerequisite. + $prerequisite = $course->get_prerequisite_id(); + if ( ! empty( $prerequisite ) ) { + $course_links['prerequisites'][] = array( + 'type' => $this->post_type, + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $prerequisite ) ), + ); + } + + // Prerequisite track. + $prerequisite_track = $course->get_prerequisite_id( 'course_track' ); + if ( ! empty( $prerequisite_track ) ) { + $course_links['prerequisites'][] = array( + 'type' => 'track', + 'href' => rest_url( sprintf( 'wp/v2/%s/%d', 'course_track', $prerequisite_track ) ), + ); + } + + // Students. + $course_links['students'] = array( + 'href' => add_query_arg( + 'enrolled_in', + $course_id, + rest_url( sprintf( '%s/%s', 'llms/v1', 'students' ) ) + ), + ); + + $links = array_merge( $links, $course_links ); + + /** + * Filters the courses's links. + * + * @since 1.0.0-beta.9 + * + * @param array $links Links for the given lesson. + * @param LLMS_Course $course Course object. + */ + return apply_filters( 'llms_rest_course_links', $links, $course ); + } + + /** + * Retrieves the query params for the enrollments objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_enrollments_collection_params() { + $query_params = $this->enrollments_controller->get_collection_params(); + + unset( $query_params['post'] ); + + $query_params['student'] = array( + 'description' => __( 'Limit results to a specific student or a list of students. Accepts a single student id or a comma separated list of student ids.', 'lifterlms' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Retrieves the query params for the sections objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_course_content_collection_params() { + + $query_params = $this->sections_controller->get_collection_params(); + + $query_params['orderby']['enum'] = array( + 'order', + 'id', + 'title', + ); + $query_params['orderby']['default'] = 'order'; + + unset( $query_params['parent'] ); + + return $query_params; + + } + + /** + * Get a collection of content items (sections). + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_course_content_items( $request ) { + + $this->sections_controller->set_parent_id( $request['id'] ); + $result = $this->sections_controller->get_items( $request ); + + // Specs require 404 when no course's sections are found. + if ( ! is_wp_error( $result ) && empty( $result->data ) ) { + return llms_rest_not_found_error(); + } + + return $result; + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-enrollments-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-enrollments-controller.php new file mode 100644 index 0000000000..028e6dc4d3 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-enrollments-controller.php @@ -0,0 +1,1243 @@ +<?php +/** + * REST Enrollments Controller. + * + * @package LLMS_REST + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.18 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Enrollments_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Don't output "Last" page link header on the last page. + * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although the single + * enrollment permission will be checked in `LLMS_REST_Enrollments_Controller::get_objects()`. + * The single enrollment can be read only by who can view the enrollment's student. + * Enrollment's post_id and student_id casted to integer, and fix calling + * to some undefined functions. + * @since 1.0.0-beta.7 `prepare_objects_query()` renamed to `prepare_collection_query_args()`. + * `prepare_object_query()` renamed to `prepare_object_query_args()`. + * Added: `get_objects_from_query()`, `prepare_objects_query()`, + * `get_pagination_data_from_query()`, `prepare_collection_items_for_response()` + * methods overrides. + * `get_items()` method removed, now abstracted in LLMS_REST_Controller. + * Fixed description of the `post_id` path parameter. + * @since 1.0.0-beta.10 Added `trigger` property and as param for creation/update/and deletion requests. + * Added `get_endpoint_args_for_item_schema()` method override. + * Use backticks in args and item schema properties descriptions where convenient. + * Filter prepared enrollment for response in order to include only fields available for response. + * Added `llms_rest_enrollments_item_schema`, `llms_rest_prepare_enrollment_object_response`, + * `llms_rest_enrollment_links` filter hooks. + * Also fix return when the enrollment to be deleted doesn't exist. + * Fixed 'context' query parameter schema. + * @since 1.0.0-beta.12 Updated `$this->prepare_collection_query_args()` to reflect changes in the parent class. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Enrollments_Controller extends LLMS_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'students/(?P<id>[\d]+)/enrollments'; + + /** + * Collection params. + * + * @var array() + */ + protected $collection_params; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'date_created', + 'date_updated', + ); + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + */ + public function __construct() { + $this->collection_params = $this->build_collection_params(); + } + + /** + * Retrieves an array of endpoint arguments from the item schema for the controller. + * + * @since 1.0.0-beta.10 + * + * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are + * checked for required values and may fall-back to a given default, this is not done + * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE. + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + + if ( in_array( $method, array( 'PATCH', 'POST', WP_REST_Server::DELETABLE ), true ) ) { + $args = array( + 'trigger' => array( + 'description' => __( 'The trigger of the enrollment to act on.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'any', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } else { + $args = parent::get_endpoint_args_for_item_schema( $method ); + } + + return $args; + + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Fixed description of the `post_id` path parameter. + * @since 1.0.0-beta.10 Add `trigger` param for create/update/delete endpoints. + * Use backticks in args descriptions. + * + * @return void + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<post_id>[\d]+)', + array( + 'args' => array( + 'post_id' => array( + 'description' => __( 'Unique course or membership Identifier. The WordPress Post `ID.`', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + array( + 'methods' => 'POST', + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( 'POST' ), + ), + array( + 'methods' => 'PATCH', + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( 'PATCH' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::DELETABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + } + + /** + * Check if a given request has access to read items. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although + * the single enrollment permission will be checked in + * `LLMS_REST_Enrollments_Controller::get_objects()`. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + + if ( stristr( $request->get_route(), '/students/' ) && isset( $request['id'] ) ) { + $enrollment = new stdClass(); + $enrollment->student_id = (int) $request['id']; + if ( ! $this->check_read_permission( $enrollment ) ) { + return llms_rest_authorization_required_error(); + } + } + + return true; + + } + + /** + * Get a collection of enrollments. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.3 Don't output "Last" page link header on the last page. + * @since 1.0.0-beta.7 Overrides the parent `get_items()` for the only purpose of returning a 404 if no enrollments are found. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + + $response = parent::get_items( $request ); + // Specs require 404 when no course enrollments are found. + if ( ! is_wp_error( $response ) && empty( $response->data ) ) { + return llms_rest_not_found_error(); + } + + return $response; + + } + + /** + * Check if a given request has access to read an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + + $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'] ); + if ( is_wp_error( $enrollment_exists ) ) { + return $enrollment_exists; + } + + $object = new stdClass(); + + $object->student_id = (int) $request['id']; + $object->post_id = (int) $request['post_id']; + + if ( ! $this->check_read_permission( $object ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + } + + /** + * Get a single item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + + $object = $this->get_object( (int) $request['id'], (int) $request['post_id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $response = $this->prepare_item_for_response( $object, $request ); + + return $response; + + } + + /** + * Check if a given request has access to create an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Handle the `trigger` param. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + + $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'], false ); + + if ( $enrollment_exists ) { + return llms_rest_bad_request_error( __( 'Cannot create existing enrollment. Use the PATCH method if you want to update an existing enrollment', 'lifterlms' ) ); + } + + if ( ! $this->check_create_permission() ) { + return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to create an enrollment as this user.', 'lifterlms' ) ); + } + + return true; + } + + + /** + * Creates a single enrollment. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Handle the `trigger` param. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + + $user_id = (int) $request['id']; + $post_id = (int) $request['post_id']; + + // The default trigger for the `LLMS_Student::enroll()` method is 'unspecified'. + $trigger = $request['trigger'] && 'any' !== $request['trigger'] ? $request['trigger'] : 'unspecified'; + + // check both students and product exist. + $student = new LLMS_Student( $user_id ); + + if ( ! $student->exists() ) { + return llms_rest_not_found_error(); + } + + // can only be enrolled in the following post types. + $product_type = get_post_type( $post_id ); + if ( ! $product_type ) { + return llms_rest_not_found_error(); + } + + if ( ! in_array( $product_type, array( 'course', 'llms_membership' ), true ) ) { + return llms_rest_bad_request_error(); + } + + // Enroll. + $enroll = $student->enroll( $post_id, $trigger ); + + // Something went wrong internally. + if ( ! $enroll ) { + return llms_rest_server_error( __( 'The enrollment could not be created', 'lifterlms' ) ); + } + + $request->set_param( 'context', 'edit' ); + $enrollment = $this->get_object( $user_id, $post_id ); + + $response = $this->prepare_item_for_response( $enrollment, $request ); + + $response->set_status( 201 ); + + $response->header( + 'Location', + rest_url( sprintf( '/%s/%s/%d/%s/%d', 'llms/v1', 'students', $enrollment->student_id, 'enrollments', $enrollment->post_id ) ) + ); + + return $response; + + } + + /** + * Check if a given request has access to update an item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Handle the `trigger` param. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + + $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] ); + if ( is_wp_error( $enrollment_exists ) ) { + return $enrollment_exists; + } + + if ( ! $this->check_update_permission() ) { + return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to update an enrollment as this user.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Update item. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Return a bad request error when supplying an invalid date_created param. + * @since 1.0.0-beta.10 Handle `trigger` param. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object or WP_Error on failure. + */ + public function update_item( $request ) { + + $student_id = (int) $request['id']; + $post_id = (int) $request['post_id']; + + // check both students and product exist. + $student = new LLMS_Student( $student_id ); + + if ( ! $student->exists() ) { + return llms_rest_not_found_error(); + } + + // can only be enrolled in the following post types. + $product_type = get_post_type( $post_id ); + if ( ! $product_type ) { + return llms_rest_not_found_error(); + } + if ( ! in_array( $product_type, array( 'course', 'llms_membership' ), true ) ) { + return llms_rest_bad_request_error(); + } + + if ( 'any' !== $request['trigger'] && $request['trigger'] !== $student->get_enrollment_trigger( $post_id ) ) { + return llms_rest_not_found_error(); + } + + $schema = $this->get_item_schema(); + + if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) { + + $updated_status = $this->handle_status_update( $student, $post_id, $request['status'], $request['trigger'] ); + + // Something went wrong internally. + if ( ! $updated_status ) { + return llms_rest_server_error( __( 'The enrollment status could not be updated', 'lifterlms' ) ); + } + } + + if ( ! empty( $schema['properties']['date_created'] ) && isset( $request['date_created'] ) ) { + + $updated_date_created = $this->handle_creation_date_update( $student_id, $post_id, $request['date_created'] ); + + if ( is_wp_error( $updated_date_created ) ) { + return $updated_date_created; + } + + // Something went wrong internally. + if ( ! $updated_date_created ) { + return llms_rest_server_error( __( 'The enrollment creation date could not be updated', 'lifterlms' ) ); + } + } + + $request->set_param( 'context', 'edit' ); + + $enrollment = $this->get_object( $student_id, $post_id ); + + $response = $this->prepare_item_for_response( $enrollment, $request ); + $response = rest_ensure_response( $response ); + return $response; + + } + + /** + * Check if a given request has access to delete an item. + * + * @since 1.0.0-beta.1 + * @since The`trigger` param is now taken into account. + * @since 1.0.0-beta.18 Provide a more significant error message when trying to delete an item without permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + + $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] ); + if ( is_wp_error( $enrollment_exists ) ) { + // Enrollment not found, we don't return a 404. + if ( in_array( 'llms_rest_not_found', $enrollment_exists->get_error_codes(), true ) ) { + return true; + } + + return $enrollment_exists; + } + + if ( ! $this->check_delete_permission() ) { + return llms_rest_authorization_required_error( + sprintf( + // Translators: %s = The post type name. + __( 'Sorry, you are not allowed to delete enrollments as this user.', 'lifterlms' ), + get_post_type_object( $this->post_type )->labels->name + ) + ); + + } + + return true; + + } + + /** + * Deletes a single llms post. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 The`trigger` param is now taken into account. + * Also fix return when the enrollment to be deleted doesn't exist. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + + $response = new WP_REST_Response(); + $response->set_status( 204 ); + + $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] ); + + if ( is_wp_error( $enrollment_exists ) ) { + // Enrollment not found, we don't return a 404. + if ( in_array( 'llms_rest_not_found', $enrollment_exists->get_error_codes(), true ) ) { + return $response; + } + + return $enrollment_exists; + } + + $result = llms_delete_student_enrollment( (int) $request['id'], (int) $request['post_id'], $request['trigger'] ); + + if ( ! $result ) { + return llms_rest_server_error( __( 'The enrollment cannot be deleted.', 'lifterlms' ) ); + } + + return rest_ensure_response( $response ); + + } + + /** + * Check enrollment existence. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Added the `trigger` param. + * + * @param int $student_id Student ID. + * @param int $post_id The course/membership ID. + * @param string $trigger Optional. The enrollment trigger. Default 'any'. + * @param boolean $wp_error Optional. Whether return a WP_Error instance or a boolean. Default true (returns WP_Error). + * @return WP_Error|boolean + */ + protected function enrollment_exists( $student_id, $post_id, $trigger = 'any', $wp_error = true ) { + + $student = llms_get_student( $student_id ); + + if ( empty( $student ) ) { + return $wp_error ? llms_rest_not_found_error() : false; + } + + $current_status = $student->get_enrollment_status( $post_id ); + + if ( empty( $current_status ) ) { + return $wp_error ? llms_rest_not_found_error() : false; + } + + if ( 'any' !== $trigger && $trigger !== $student->get_enrollment_trigger( $post_id ) ) { + return $wp_error ? llms_rest_not_found_error() : false; + } + + return true; + + } + + /** + * Get object. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Fix call to undefined function llms_rest_bad_request(), + * must be llms_rest_bad_request_error(). + * + * @param int $student_id Student ID. + * @param int $post_id The course/membership ID. + * @return object|WP_Error + */ + protected function get_object( $student_id, $post_id = null ) { + + if ( empty( $post_id ) ) { + return llms_rest_bad_request_error(); + } + + $query_args = $this->prepare_object_query_args( $student_id, $post_id ); + $query = $this->get_objects_query( $query_args ); + $items = $this->get_objects_from_query( $query ); + + if ( $items ) { + return $items[0]; + } + + return llms_rest_not_found_error(); + } + + /** + * Prepare enrollments objects query. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.10 Set query limit to 1. + * + * @param int $student_id Student ID. + * @param int $post_id The course/membership ID. + * @return array + */ + protected function prepare_object_query_args( $student_id, $post_id ) { + + $args = array(); + + $args['id'] = $student_id; + $args['post'] = $post_id; + $args['no_found_rows'] = true; + $args['per_page'] = 1; + + $args = $this->prepare_items_query( $args ); + + return $args; + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array The Enrollments collection parameters. + */ + public function get_collection_params() { + return $this->collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @param array $collection_params The Enrollments collection parameters to be set. + * @return void + */ + public function set_collection_params( $collection_params ) { + $this->collection_params = $collection_params; + } + + /** + * Build the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Fixed 'context' query parameter schema. + * + * @return array Collection parameters. + */ + protected function build_collection_params() { + + $query_params = parent::get_collection_params(); + + unset( $query_params['include'], $query_params['exclude'] ); + + $query_params['status'] = array( + 'description' => __( 'Filter results to records matching the specified status.', 'lifterlms' ), + 'enum' => array_keys( llms_get_enrollment_statuses() ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + $query_params['post'] = array( + 'description' => __( 'Limit results to a specific course or membership or a list of courses and/or memberships. Accepts a single post id or a comma separated list of post ids.', 'lifterlms' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Get the Enrollments's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Added the `trigger` property. + * Added backticks in properties description where convenient. + * Added `llms_rest_enrollments_item_schema` filter hook. + * @return array + */ + public function get_item_schema() { + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'students-enrollments', + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'description' => __( 'The ID of the course/membership.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'student_id' => array( + 'description' => __( 'The ID of the student.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'Creation date. Format: `Y-m-d H:i:s`', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_updated' => array( + 'description' => __( 'Date last modified. Format: `Y-m-d H:i:s`', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'The status of the enrollment.', 'lifterlms' ), + 'enum' => array_keys( llms_get_enrollment_statuses() ), + 'context' => array( 'view', 'edit' ), + 'type' => 'string', + ), + 'trigger' => array( + 'description' => __( 'The enrollment trigger. Default is `any`.', 'lifterlms' ), + 'context' => array( 'view', 'edit' ), + 'type' => 'string', + 'default' => 'any', + 'readonly' => true, + ), + ), + ); + + /** + * Filter item schema for the enrollments controller. + * + * @since 1.0.0-beta.10 + * + * @param array $schema Item schema data. + */ + return apply_filters( 'llms_rest_enrollments_item_schema', $schema ); + + } + + /** + * Retrieve an array of objects from the result of $this->get_objects_query(). + * + * @since 1.0.0-beta.7 + * + * @param WP_Query $query Query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + return $query->items; + + } + + /** + * Prepare collection items for response. + * + * @since 1.0.0-beta.7 + * + * @param array $objects Array of objects to be prepared for response. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + + foreach ( $objects as $object ) { + + if ( ! $this->check_read_permission( $object ) ) { + continue; + } + + $item = $this->prepare_item_for_response( $object, $request ); + if ( ! is_wp_error( $item ) ) { + $items[] = $this->prepare_response_for_collection( $item ); + } + } + + return $items; + } + + /** + * Retrieve pagination information from an objects query. + * + * @since 1.0.0-beta.7 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + $total_results = (int) $query->found_results; + $current_page = isset( $prepared['page'] ) ? (int) $prepared['page'] : 1; + $total_pages = (int) ceil( $total_results / (int) $prepared['per_page'] ); + + return compact( 'current_page', 'total_results', 'total_pages' ); + + } + + /** + * Prepare enrollments objects query + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.12 Updated to reflect changes in the parent class. + * @since 1.0.0-beta.18 Correctly return errors. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $prepared = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $prepared ) ) { + return $prepared; + } + + $prepared['id'] = $request['id']; + $prepared['page'] = ! isset( $prepared['page'] ) ? 1 : $prepared['page']; + + return $this->prepare_items_query( $prepared, $request ); + + } + + /** + * Determines the allowed query_vars for a get_items() response and prepares + * them for WP_Query. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param WP_REST_Request $request Optional. Full details about the request. + * @return array Items query arguments. + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $query_args = array(); + + foreach ( $prepared_args as $key => $value ) { + $query_args[ $key ] = $value; + } + + // Filters. + if ( isset( $query_args['student'] ) && ! is_array( $query_args['student'] ) ) { + $query_args['student'] = array_map( 'absint', explode( ',', $query_args['student'] ) ); + } + if ( isset( $query_args['post'] ) && ! is_array( $query_args['post'] ) ) { + $query_args['post'] = array_map( 'absint', explode( ',', $query_args['post'] ) ); + } + + if ( isset( $query_args['orderby'] ) ) { + switch ( $query_args['orderby'] ) { + case 'date_updated': + $query_args['orderby'] = 'upm2.updated_date'; + break; + case 'date_created': + $query_args['orderby'] = 'upm.updated_date'; + break; + default: + unset( $query_args['orderby'] ); + break; + } + } + + $query_args['is_students_route'] = $request ? false !== stristr( $request->get_route(), '/students/' ) : true; + + return $query_args; + + } + + /** + * Get enrollments query + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Enrollment's post_id and student_id casted to integer. + * @since 1.0.0-beta.10 Added subquery to retrive the enrollments trigger. + * @since 1.0.0-beta.18 Fixed wrong trigger retrieved when multiple trigger were present for the same user,post pair. + * + * @param array $query_args Array of collection arguments. + * @param WP_REST_Request $request Optional. Full details about the request. Defaut null. + * @return object An object with two fields: items an array of OBJECT result of the query; found_reults the total found items + */ + protected function get_objects_query( $query_args, $request = null ) { + + global $wpdb; + + // Maybe limit the query results depending on the page param. + if ( isset( $query_args['page'] ) ) { + $skip = $query_args['page'] > 1 ? ( $query_args['page'] - 1 ) * $query_args['per_page'] : 0; + $limit = $wpdb->prepare( + 'LIMIT %d, %d', + array( + $skip, + $query_args['per_page'], + ) + ); + } else { + $limit = $wpdb->prepare( + 'LIMIT %d', + $query_args['per_page'] + ); + } + + /** + * List enrollments of the current student_id or post_id. + * Depends on the endpoint route. + */ + if ( $query_args['is_students_route'] ) { + $id_column = 'user_id'; + } else { + $id_column = 'post_id'; + } + + /** + * Filter the enrollments by user_id or post_id param + */ + if ( isset( $query_args['student'] ) ) { + $filter = sprintf( ' AND upm.user_id IN ( %s )', implode( ', ', $query_args['student'] ) ); + } elseif ( isset( $query_args['post'] ) ) { + $filter = sprintf( ' AND upm.post_id IN ( %s )', implode( ', ', $query_args['post'] ) ); + } else { + $filter = ''; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $updated_date_status = $wpdb->prepare( + "( + SELECT DISTINCT user_id, post_id, updated_date, meta_value + FROM {$wpdb->prefix}lifterlms_user_postmeta as upm + WHERE upm.{$id_column} = %d + $filter AND upm.meta_key = '_status' + AND upm.updated_date = ( + SELECT MAX( upm2.updated_date ) + FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2 + WHERE upm2.meta_key = '_status' + AND upm2.post_id = upm.post_id + AND upm2.user_id = upm.user_id + ) + )", + array( + $query_args['id'], + ) + ); + + // Trigger. + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $trigger = $wpdb->prepare( + "( + SELECT DISTINCT user_id, post_id, meta_value + FROM {$wpdb->prefix}lifterlms_user_postmeta as upm + WHERE upm.{$id_column} = %d + $filter AND upm.meta_key = '_enrollment_trigger' + AND upm.updated_date = ( + SELECT MAX( upm2.updated_date ) + FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2 + WHERE upm2.meta_key = '_enrollment_trigger' + AND upm2.post_id = upm.post_id + AND upm2.user_id = upm.user_id + ) + )", + array( + $query_args['id'], + ) + ); + + if ( isset( $query_args['status'] ) ) { + $filter .= $wpdb->prepare( ' AND upm2.meta_value = %s', $query_args['status'] ); + } + + if ( isset( $query_args['orderby'], $query_args['order'] ) ) { + $order = sprintf( 'ORDER BY %1$s %2$s', esc_sql( $query_args['orderby'] ), esc_sql( $query_args['order'] ) ); + } else { + $order = ''; + } + + $query = new stdClass(); + + $select_found_rows = empty( $query_args['no_found_rows'] ) ? esc_sql( 'SQL_CALC_FOUND_ROWS' ) : ''; + + // the query. + $query->items = $wpdb->get_results( + $wpdb->prepare( + " + SELECT {$select_found_rows} DISTINCT upm.post_id AS post_id, upm.user_id as student_id, upm.updated_date as date_created, upm2.updated_date as date_updated, upm2.meta_value as status, upm3.meta_value as etrigger + FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm + JOIN {$updated_date_status} as upm2 ON upm.post_id = upm2.post_id AND upm.user_id = upm2.user_id + JOIN {$trigger} as upm3 ON upm.post_id = upm3.post_id AND upm.user_id = upm3.user_id + WHERE upm.meta_key = '_start_date' + AND upm.{$id_column} = %d + {$filter} + {$order} + {$limit}; + ", + array( + $query_args['id'], + ) + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + $count = count( $query->items ); + + if ( $count ) { + foreach ( $query->items as $key => $item ) { + $query->items[ $key ]->post_id = (int) $item->post_id; + $query->items[ $key ]->student_id = (int) $item->student_id; + $query->items[ $key ]->trigger = (string) $item->etrigger; + unset( $query->items[ $key ]->etrigger ); + } + } + + $query->found_results = empty( $query_args['no_found_rows'] ) ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : $count; + + return $query; + + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Filter enrollment to include only fields available for response. + * Added `llms_rest_prepare_enrollment_object_response` filter hook. + * + * @param stdClass $enrollment Enrollment object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + public function prepare_object_for_response( $enrollment, $request ) { + + $prepared_enrollment = get_object_vars( $enrollment ); + + // Apply filters. + $prepared_enrollment['status'] = apply_filters( + 'llms_get_enrollment_status', + $prepared_enrollment['status'], + $prepared_enrollment['student_id'], + $prepared_enrollment['post_id'] + ); + + // Filter data including only schema props. + $data = array_intersect_key( $prepared_enrollment, array_flip( $this->get_fields_for_response( $request ) ) ); + + /** + * Filters the enrollment data for a response. + * + * @since 1.0.0-beta.10 + * + * @param array $data Array of enrollment properties prepared for response. + * @param stdClass $enrollment Enrollment object. + * @param WP_REST_Request $request Full details about the request. + */ + return apply_filters( 'llms_rest_prepare_enrollment_object_response', $data, $enrollment, $request ); + } + + /** + * Prepare enrollments links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added $request parameter. + * + * @param object $enrollment Enrollment object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + public function prepare_links( $enrollment, $request ) { + + $links = array( + 'self' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d/%s/%d', 'llms/v1', 'students', $enrollment->student_id, 'enrollments', $enrollment->post_id ) + ), + ), + 'collection' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d/%s', 'llms/v1', 'students', $enrollment->student_id, 'enrollments' ) + ), + ), + 'student' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'students', $enrollment->student_id ) + ), + ), + ); + + switch ( get_post_type( $enrollment->post_id ) ) : + case 'course': + $links['post'] = array( + 'type' => 'course', + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $enrollment->post_id ) + ), + ); + break; + + case 'llms_membership': + $links['post'] = array( + 'type' => 'llms_membership', + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'memberships', $enrollment->post_id ) + ), + ); + break; + endswitch; + + /** + * Filters the enrollment's links. + * + * @since 1.0.0-beta.10 + * + * @param array $links Links for the given enrollment. + * @param stdClass $enrollment Enrollment object. + */ + return apply_filters( 'llms_rest_enrollment_links', $links, $enrollment ); + + } + + /** + * Handles the enrollment status update. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.10 Added the `trigger` paramater. + * + * @param LLMS_Student $student Student. + * @param integer $post_id The post id. + * @param string $status Status. + * @param string $trigger The enrollment trigger. + * @return boolean + */ + protected function handle_status_update( $student, $post_id, $status, $trigger ) { + + // Status. + switch ( $status ) : + case 'enrolled': + // The default trigger for the `LLMS_Student::enroll()` method is 'unspecified'. + $trigger = $trigger && 'any' !== $trigger ? $trigger : 'unspecified'; + $updated = $student->enroll( $post_id, 'admin_' . get_current_user_id(), $trigger ); + break; + default: + $updated = $student->unenroll( $post_id, $trigger, $status ); + endswitch; + + return $updated; + + } + + + /** + * Handles the enrollment creation date. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`. + * + * @param integer $student_id Student id. + * @param integer $post_id The post id. + * @param string $date Creation date. + * @return boolean + */ + protected function handle_creation_date_update( $student_id, $post_id, $date ) { + + $date_created = rest_parse_date( $date ); + + if ( ! $date_created ) { + return llms_rest_bad_request_error(); + } + + $date_created = date_i18n( 'Y-m-d H:i:s', $date_created ); + + global $wpdb; + + $inner_query = $wpdb->prepare( + " + SELECT upm2.meta_id + FROM ( SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta ) AS upm2 + WHERE upm2.meta_key = '_start_date' AND upm2.user_id = %d AND upm2.post_id = %d + ORDER BY upm2.updated_date DESC + LIMIT 1 + ", + $student_id, + $post_id + ); + + $result = $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->prefix}lifterlms_user_postmeta SET updated_date = %s WHERE meta_id = (${inner_query});", + $date_created + ) + ); // no-cache ok. + + return $result; + } + + /** + * Checks if an enrollment can be edited. + * + * @since 1.0.0-beta.1 + * + * @return bool Whether the enrollment can be created + */ + protected function check_create_permission() { + return current_user_can( 'enroll' ); + } + + /** + * Checks if an enrollment can be updated + * + * @since 1.0.0-beta.1 + * + * @return bool Whether the enrollment can be edited. + */ + protected function check_update_permission() { + return current_user_can( 'enroll' ) && current_user_can( 'unenroll' ); + } + + /** + * Checks if an enrollment can be deleted + * + * @since 1.0.0-beta.1 + * + * @return bool Whether the enrollment can be deleted. + */ + protected function check_delete_permission() { + return current_user_can( 'unenroll' ); + } + + /** + * Checks if an enrollment can be read. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.4 The single enrollment can be read only by who can view the enrollment's student. + * + * @param mixed $enrollment The enrollment object. + * @return bool Whether the enrollment can be read. + */ + protected function check_read_permission( $enrollment ) { + + /** + * As of now, enrollments of password protected courses cannot be read + */ + if ( isset( $enrollment->post_id ) && post_password_required( $enrollment->post_id ) ) { + return false; + } + + if ( get_current_user_id() === (int) $enrollment->student_id ) { + return true; + } + + return current_user_can( 'view_students', $enrollment->student_id ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-instructors-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-instructors-controller.php new file mode 100644 index 0000000000..62bb5cd8f2 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-instructors-controller.php @@ -0,0 +1,263 @@ +<?php +/** + * REST Resource Controller for Instructors + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Instructors_Controller class + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.13 Fixed authentication error messages referring to 'students' or 'users' rather than 'instructors'. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Instructors_Controller extends LLMS_REST_Users_Controller { + + /** + * Resource ID or Name. + * + * @var string + */ + protected $resource_name = 'instructor'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'instructors'; + + /** + * Determine if the current user can view the requested student. + * + * @since 1.0.0-beta.1 + * + * @param int $item_id WP_User id. + * @return bool + */ + protected function check_read_item_permissions( $item_id ) { + + if ( get_current_user_id() === $item_id ) { + return true; + } + + return current_user_can( 'list_users', $item_id ); + + } + + /** + * Determine if current user has permission to create a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function create_item_permissions_check( $request ) { + + if ( ! current_user_can( 'create_users' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to create new instructors.', 'lifterlms' ) ); + } + + return $this->check_roles_permissions( $request ); + + } + + /** + * Determine if current user has permission to delete a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + if ( ! current_user_can( 'delete_users', $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to delete this instructor.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $params = parent::get_collection_params(); + + $params['post_in'] = array( + 'description' => __( 'Retrieve only instructors for the specified course(s) and/or membership(s). Accepts a single WP Post ID or a comma separated list of IDs.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + + $params['post_not_in'] = array( + 'description' => __( 'Exclude instructors who do not have permissions for the specified course(s) and/or membership(s). Accepts a single WP Post ID or a comma separated list of IDs.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + + return $params; + + } + + /** + * Determine if current user has permission to get a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function get_item_permissions_check( $request ) { + + if ( ! $this->check_read_item_permissions( $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to view this instructor.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Get the item schema. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_item_schema() { + + $schema = parent::get_item_schema(); + + $schema['properties']['roles']['default'] = array( 'instructor' ); + + return $schema; + + } + + /** + * Determine if current user has permission to list users. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.13 Fixed authentication error message referring to 'students' rather than 'instructors'. + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function get_items_permissions_check( $request ) { + + if ( ! current_user_can( 'list_users' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to list instructors.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Get object. + * + * @since 1.0.0-beta.1 + * + * @param int $id Object ID. + * @return LLMS_Instructor|WP_Error + */ + protected function get_object( $id ) { + + $instructor = llms_get_instructor( $id ); + return $instructor ? $instructor : llms_rest_not_found_error(); + + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added the `$request` parameter. + * + * @param obj $object Item object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $object, $request ) { + + $links = parent::prepare_links( $object, $request ); + + $links['content'] = array( + 'href' => sprintf( '%s/content', $links['self']['href'] ), + ); + + return $links; + + } + + /** + * Updates additional information not handled by WP Core insert/update user functions. + * + * @since 1.0.0-beta.1 + * + * @param int $object_id WP User id. + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return LLMS_Abstract_User_Data|WP_error + */ + protected function update_additional_data( $object_id, $prepared, $request ) { + + $object = parent::update_additional_data( $object_id, $prepared, $request ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Add a parent_id of the current user when creating an instructors_assistant. + // @todo: this should actually be handled by a `parent_ids` create/update arg required when assistant is a supplied role. + if ( get_current_user_id() !== $object_id && ! empty( $prepared['roles'] ) && in_array( 'instructors_assistant', $prepared['roles'], true ) ) { + $object->add_parents( get_current_user_id() ); + } + + return $object; + + } + + /** + * Determine if current user has permission to update a user. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.13 Refer to the instructor role on the authorization error message rather than the generic 'user'. + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function update_item_permissions_check( $request ) { + + if ( get_current_user_id() === $request['id'] ) { + return true; + } + + if ( ! current_user_can( 'edit_users', $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to edit this instructor.', 'lifterlms' ) ); + } + + return $this->check_roles_permissions( $request ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-lessons-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-lessons-controller.php new file mode 100644 index 0000000000..979db9bfaa --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-lessons-controller.php @@ -0,0 +1,830 @@ +<?php +/** + * REST Lessons Controller + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.21 + */ + +defined( 'ABSPATH' ) || exit; + + +/** + * LLMS_REST_Lessons_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 `prepare_objects_query()` renamed to `prepare_collection_query_args()`. + * Added the following properties to the item schema: `drip_date`, `drip_days`, `drip_method`, `public`, `quiz`. + * Added the following links: `prerequisite`, `quiz`. + * Fixed `siblings` link that was using the parent course's id instead of the parent section's id. + * Fixed `parent` link href, replacing 'section' with 'sections'. + * Added following properties to the response object: `public`, `points`, `quiz`, `drip_method`, `drip_days`, `drip_date`, `prerequisite`. + * Fixed lesson progression callback name when defining the filters to be removed while preparing the item for response. + * Added `llms_rest_lesson_item_schema`, `llms_rest_pre_insert_lesson`, `llms_rest_prepare_lesson_object_response`, `llms_rest_lesson_links` filter hooks. + * Added `prepare_item_for_database()`, `update_additional_object_fields()` method. + * @since 1.0.0-beta.8 Call `set_bulk()` llms post method passing `true` as second parameter, + * so to instruct it to return a WP_Error on failure. + * @since 1.0.0-beta.9 Removed `create_llms_post()` and `get_object()` methods, now abstracted in `LLMS_REST_Posts_Controller` class. + * `llms_rest_lesson_filters_removed_for_response` filter hook added. + * @since 1.0.0-beta.12 Updated `$this->prepare_collection_query_args()` to reflect changes in the parent class. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Lessons_Controller extends LLMS_REST_Posts_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'lessons'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'lesson'; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'title', + 'date_created', + 'date_updated', + 'order', + 'relevance', + ); + + /** + * Parent section id. + * + * @var int + */ + protected $parent_id; + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + */ + public function __construct() { + + $this->collection_params = $this->build_collection_params(); + + } + + /** + * Set parent id. + * + * @since 1.0.0-beta.1 + * + * @param int $parent_id Course parent id. + * @return void + */ + public function set_parent_id( $parent_id ) { + $this->parent_id = $parent_id; + } + + /** + * Get parent id. + * + * @since 1.0.0-beta.1 + * + * @return int|null Course parent id. Null if not set. + */ + public function get_parent_id() { + return isset( $this->parent_id ) ? $this->parent_id : null; + } + + /** + * Prepares a single lesson for create or update. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.15 Fixed setting/updating parent section/course. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of lesson args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + + $prepared_item = parent::prepare_item_for_database( $request ); + $schema = $this->get_item_schema(); + + // Lesson's audio embed URL. + if ( ! empty( $schema['properties']['audio_embed'] ) && isset( $request['audio_embed'] ) ) { + $prepared_item['audio_embed'] = $request['audio_embed']; + } + + // Lesson's video embed URL. + if ( ! empty( $schema['properties']['video_embed'] ) && isset( $request['video_embed'] ) ) { + $prepared_item['video_embed'] = $request['video_embed']; + } + + // Parent (section) id. + if ( ! empty( $schema['properties']['parent_id'] ) && isset( $request['parent_id'] ) ) { + + // Retrieve the parent section. + $parent_section = llms_get_post( $request['parent_id'] ); + + $prepared_item['parent_section'] = $parent_section && is_a( $parent_section, 'LLMS_Section' ) ? $request['parent_id'] : 0; + + // Retrive the parent course id. + if ( $prepared_item['parent_section'] ) { + $parent_course = $parent_section->get_course(); + } + + $prepared_item['parent_course'] = ! empty( $parent_course ) && is_a( $parent_course, 'LLMS_Course' ) ? $parent_course->get( 'id' ) : 0; + + /** + * The parent course is 'derivate', we need to be sure that, if updating, the new value is different from the previous one + * otherwise the underlying wp function `update_post_meta()` will return `false`. + */ + if ( $request['id'] ) { + $lesson = $this->get_object( $request['id'] ); + if ( $lesson && $parent_course_id === $lesson->get_parent_course() ) { + unset( $prepared_item['parent_course'] ); + } + } + } + + // Course id. + if ( ! empty( $schema['properties']['course_id'] ) && isset( $request['course_id'] ) ) { + + $parent_course = llms_get_post( $request['course_id'] ); + + if ( ! $parent_course || ! is_a( $parent_course, 'LLMS_Course' ) ) { + return llms_rest_bad_request_error( __( 'Invalid course_id param. It must be a valid Course ID.', 'lifterlms' ) ); + } + + $prepared_item['parent_course'] = $request['course_id']; + } + + // Order. + if ( ! empty( $schema['properties']['order'] ) && isset( $request['order'] ) ) { + + // order must be > 0. It's sanitized as absint so it cannot come as negative value. + if ( 0 === $request['order'] ) { + return llms_rest_bad_request_error( __( 'Invalid order param. It must be greater than 0.', 'lifterlms' ) ); + } + + $prepared_item['order'] = $request['order']; + } + + // Public (free lesson). + if ( ! empty( $schema['properties']['public'] ) && isset( $request['public'] ) ) { + $prepared_item['free_lesson'] = empty( $request['public'] ) ? 'no' : 'yes'; + } + + // Points. + if ( ! empty( $schema['properties']['points'] ) && isset( $request['points'] ) ) { + $prepared_item['points'] = $request['points']; + } + + // Drip days. + if ( ! empty( $schema['properties']['drip_days'] ) && isset( $request['drip_days'] ) ) { + + // drip_days must be > 0. It's sanitized as absint so it cannot come as negative value. + if ( 0 === $request['drip_days'] ) { + return llms_rest_bad_request_error( __( 'Invalid drip_days param. It must be greater than 0.', 'lifterlms' ) ); + } + + $prepared_item['days_before_available'] = $request['drip_days']; + } + + // Drip date. + if ( ! empty( $schema['properties']['drip_date'] ) && isset( $request['drip_date'] ) ) { + $drip_date = rest_parse_date( $request['drip_date'] ); + + // Drip date is nullable. + if ( empty( $drip_date ) ) { + $prepared_item['date_available'] = ''; + $prepared_item['time_available'] = ''; + } else { + $prepared_item['date_available'] = date_i18n( 'Y-m-d', $drip_date ); + $prepared_item['time_available'] = date_i18n( 'H:i:s', $drip_date ); + } + } + + // Drip method. + if ( ! empty( $schema['properties']['drip_method'] ) && isset( $request['drip_method'] ) ) { + $prepared_item['drip_method'] = 'none' === $request['drip_method'] ? '' : $request['drip_method']; + } + + // Quiz enabled. + if ( ! empty( $schema['properties']['quiz']['properties']['enabled'] ) && isset( $request['quiz']['enabled'] ) ) { + $prepared_item['quiz_enabled'] = empty( $request['quiz']['enabled'] ) ? 'no' : 'yes'; + } + + // Quiz id. + if ( ! empty( $schema['properties']['quiz']['properties']['id'] ) && isset( $request['quiz']['id'] ) ) { + + // check if quiz exists. + $quiz = llms_get_post( $request['quiz']['id'] ); + + if ( is_a( $quiz, 'LLMS_Quiz' ) ) { + $prepared_item['quiz'] = $request['quiz']['id']; + } + } + + // Quiz progression. + if ( ! empty( $schema['properties']['quiz']['properties']['progression'] ) && isset( $request['quiz']['progression'] ) ) { + $prepared_item['require_passing_grade'] = 'complete' === $request['quiz']['progression'] ? 'no' : 'yes'; + } + + /** + * Filters a lesson before it is inserted via the REST API. + * + * @since 1.0.0-beta.7 + * + * @param array $prepared_item Array of lesson item properties prepared for database. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + */ + return apply_filters( 'llms_rest_pre_insert_lesson', $prepared_item, $request, $schema ); + + } + + /** + * Updates a single llms lesson. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.8 Call `set_bulk()` llms post method passing `true` as second parameter, + * so to instruct it to return a WP_Error on failure. + * + * @param LLMS_Lesson $lesson LLMS_Lesson instance. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + * @param array $prepared_item Array. + * @param bool $creating Optional. Whether we're in creation or update phase. Default true (create). + * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update. + */ + protected function update_additional_object_fields( $lesson, $request, $schema, $prepared_item, $creating = true ) { + + $error = new WP_Error(); + + $to_set = array(); + + // Prerequisite. + if ( ! empty( $schema['properties']['prerequisite'] ) && isset( $request['prerequisite'] ) ) { + + // check if lesson exists. + $prerequisite = llms_get_post( $request['prerequisite'] ); + + if ( is_a( $prerequisite, 'LLMS_Lesson' ) ) { + $to_set['prerequisite'] = $request['prerequisite']; + } else { + $to_set['prerequisite'] = 0; + } + } + + // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + $to_set['has_prerequisite'] = empty( $to_set['prerequisite'] ) ? 'no' : 'yes'; + + if ( ! $creating ) { + if ( $to_set['has_prerequisite'] === $lesson->get( 'has_prerequisite' ) ) { + unset( $to_set['has_prerequisite'] ); + } + } + + // Set bulk. + if ( ! empty( $to_set ) ) { + $update = $lesson->set_bulk( $to_set, true ); + if ( is_wp_error( $update ) ) { + $error = $update; + } + } + + if ( ! empty( $error->errors ) ) { + return $error; + } + + return ! empty( $to_set ); + + } + + /** + * Get the Lesson's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added the following properties: drip_date, drip_days, drip_method, public, quiz. + * Added `llms_rest_lesson_item_schema` filter hook. + * @since 1.0.0-beta.15 Fixed `course_id` property access: it must be read-only. + * + * @return array Item schema data. + */ + public function get_item_schema() { + + $schema = parent::get_item_schema(); + + $lesson_properties = array( + 'parent_id' => array( + 'description' => __( 'WordPress post ID of the parent item. Must be a Section ID. 0 indicates an "orphaned" lesson which can be edited and viewed by instructors and admins but cannot be read by students.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'course_id' => array( + 'description' => __( 'WordPress post ID of the lesson\'s parent course.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + 'readonly' => true, + ), + 'order' => array( + 'description' => __( 'Order of the lesson within its immediate parent.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + 'required' => true, + ), + 'prerequisite' => array( + 'description' => __( 'Lesson ID of the prerequisite lesson.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'points' => array( + 'description' => __( 'Determines the weight of the lesson when grading the course.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'audio_embed' => array( + 'description' => __( 'URL to an oEmbed enable audio URL.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'video_embed' => array( + 'description' => __( 'URL to an oEmbed enable video URL.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'drip_date' => array( + 'description' => __( + 'The date and time when the lesson becomes available. Applicable only when drip_method is date. Format: Y-m-d H:i:s.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'drip_days' => array( + 'description' => __( 'Number of days to wait before allowing access to the lesson. Applicable only when drip_method is enrollment, start, or prerequisite.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'drip_method' => array( + 'description' => __( + 'Determine the method with which to make the lesson content available. + <ul> + <li>none: Drip is disabled; the lesson is immediately available.</li> + <li>date: Lesson is made available at a specific date and time.</li> + <li>enrollment: Lesson is made available a specific number of days after enrollment into the course.</li> + <li>start: Lesson is made available a specific number of days after the course\'s start date. Only available on courses with a access_opens_date.</li> + <li>prerequisite: Lesson is made available a specific number of days after the prerequisite lesson is completed.</li> + </ul>', + 'lifterlms' + ), + 'type' => 'string', + 'default' => 'none', + 'enum' => array( 'none', 'date', 'enrollment', 'start', 'prerequisite' ), + 'context' => array( 'view', 'edit' ), + ), + 'public' => array( + 'description' => __( 'Denotes a lesson that\'s publicly accessible regardless of course enrollment.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'quiz' => array( + 'description' => __( 'Associate a quiz with this lesson.', 'lifterlms' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'enabled' => array( + 'description' => __( 'Determines if a quiz is enabled for the lesson.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'id' => array( + 'description' => __( 'The post ID of the associated quiz.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'progression' => array( + 'description' => __( + 'Determines lesson progression requirements related to the quiz. + <ul> + <li>complete: The quiz must be completed (with any grade) to progress the lesson.</li> + <li>pass: A passing grade must be earned to progress the lesson.</li> + </ul>', + 'lifterlms' + ), + 'type' => 'string', + 'default' => 'complete', + 'enum' => array( 'complete', 'pass' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ); + + $schema['properties'] = array_merge( (array) $schema['properties'], $lesson_properties ); + + /** + * Filter item schema for the lessons controller. + * + * @since 1.0.0-beta.7 + * + * @param array $schema Item schema data. + */ + return apply_filters( 'llms_rest_lesson_item_schema', $schema ); + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array The Enrollments collection parameters. + */ + public function get_collection_params() { + return $this->collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @param array $collection_params The Enrollments collection parameters to be set. + * @return void + */ + public function set_collection_params( $collection_params ) { + $this->collection_params = $collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function build_collection_params() { + + $query_params = parent::get_collection_params(); + + $query_params['parent'] = array( + 'description' => __( 'Filter lessons by the parent post (section) ID.', 'lifterlms' ), + 'type' => 'integer', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added following properties to the response object: + * public, points, quiz, drip_method, drip_days, drip_date, prerequisite, audio_embed, video_embed. + * Added `llms_rest_prepare_lesson_object_response` filter hook. + * + * @param LLMS_Lesson $lesson Lesson object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $lesson, $request ) { + + $data = parent::prepare_object_for_response( $lesson, $request ); + + // Audio Embed. + $data['audio_embed'] = $lesson->get( 'audio_embed' ); + + // Video Embed. + $data['video_embed'] = $lesson->get( 'video_embed' ); + + // Parent section. + $data['parent_id'] = $lesson->get_parent_section(); + + // Parent course. + $data['course_id'] = $lesson->get_parent_course(); + + // Order. + $data['order'] = $lesson->get( 'order' ); + + // Public. + $data['public'] = $lesson->is_free(); + + // Points. + $data['points'] = $lesson->get( 'points' ); + + // Quiz. + $data['quiz']['enabled'] = llms_parse_bool( $lesson->get( 'quiz_enabled' ) ); + $data['quiz']['id'] = absint( $lesson->get( 'quiz' ) ); + $data['quiz']['progression'] = llms_parse_bool( $lesson->get( 'require_passing_grade' ) ) ? 'pass' : 'completed'; + + // Drip method. + $data['drip_method'] = $lesson->get( 'drip_method' ); + $data['drip_method'] = $data['drip_method'] ? $data['drip_method'] : 'none'; + + // Drip days. + $data['drip_days'] = absint( $lesson->get( 'days_before_available' ) ); + + // Drip date. + $date = $lesson->get( 'date_available' ); + if ( $date ) { + $time = $lesson->get( 'time_available' ); + + if ( ! $time ) { + $time = '12:00 AM'; + } + + $drip_date = date_i18n( 'Y-m-d H:i:s', strtotime( $date . ' ' . $time ) ); + } else { + $drip_date = ''; + } + + $data['drip_date'] = $drip_date; + + // Prerequisite. + $data['prerequisite'] = absint( $lesson->get_prerequisite() ); + + /** + * Filters the lesson data for a response. + * + * @since 1.0.0-beta.7 + * + * @param array $data Array of lesson properties prepared for response. + * @param LLMS_Lesson $lesson Lesson object. + * @param WP_REST_Request $request Full details about the request. + */ + return apply_filters( 'llms_rest_prepare_lesson_object_response', $data, $lesson, $request ); + + } + + /** + * Format query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.12 Updated to reflect changes in the parent class. + * @since 1.0.0-beta.18 Correctly return errors. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $query_args = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $query_args ) ) { + return $query_args; + } + + // Orderby 'order' requires a meta query. + if ( isset( $query_args['orderby'] ) && 'order' === $query_args['orderby'] ) { + $query_args = array_merge( + $query_args, + array( + 'meta_key' => '_llms_order', + 'orderby' => 'meta_value_num', + ) + ); + } + + if ( isset( $this->parent_id ) ) { + $parent_id = $this->parent_id; + } elseif ( ! empty( $request['parent'] ) && $request['parent'] > 1 ) { + $parent_id = $request['parent']; + } + + // Filter by parent. + if ( ! empty( $parent_id ) ) { + $query_args = array_merge( + $query_args, + array( + 'meta_query' => array( + array( + 'key' => '_llms_parent_section', + 'value' => $parent_id, + 'compare' => '=', + ), + ), + ) + ); + } + + return $query_args; + } + + /** + * Get action/filters to be removed before preparing the item for response. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Fixed lesson progression callback name. + * @since 1.0.0-beta.9 `llms_rest_lesson_filters_removed_for_response` filter hook added. + * + * @param LLMS_Lesson $lesson Lesson object. + * @return array Array of action/filters to be removed for response. + */ + protected function get_filters_to_be_removed_for_response( $lesson ) { + + $filters = array(); + + if ( llms_blocks_is_post_migrated( $lesson->get( 'id' ) ) ) { + $filters = array( + // hook => [callback, priority]. + 'lifterlms_single_lesson_after_summary' => array( + // Lesson Navigation. + array( + 'callback' => 'lifterlms_template_lesson_navigation', + 'priority' => 20, + ), + // Lesson Progression. + array( + 'callback' => 'lifterlms_template_complete_lesson_link', + 'priority' => 10, + ), + ), + ); + } + + /** + * Modify the array of filters to be removed before building the response. + * + * @since 1.0.0-beta.9 + * + * @param array $filters Array of filters to be removed. + * @param LLMS_Lesson $lesson Lesson object. + */ + return apply_filters( 'llms_rest_lesson_filters_removed_for_response', $filters, $lesson ); + + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Fixed `siblings` link that was using the parent course's id instead of the parent section's id. + * Fixed `parent` link href, replacing 'section' with 'sections'. + * Following links added: `prerequisite`, `quiz`. + * Added `llms_rest_lesson_links` filter hook. + * @since 1.0.0-beta.14 Added `$request` parameter. + * + * @param LLMS_Lesson $lesson LLMS Section. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $lesson, $request ) { + + $links = parent::prepare_links( $lesson, $request ); + + unset( $links['content'] ); + + $lesson_id = $lesson->get( 'id' ); + $parent_course_id = $lesson->get_parent_course(); + $parent_section_id = $lesson->get_parent_section(); + + $lesson_links = array(); + + // Parent course. + if ( $parent_course_id ) { + $lesson_links['course'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $parent_course_id ) ), + ); + } + + // Parent section. + if ( $parent_section_id ) { + $lesson_links['parent'] = array( + 'type' => 'section', + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'sections', $parent_section_id ) ), + ); + } + + // Siblings. + $lesson_links['siblings'] = array( + 'href' => add_query_arg( + 'parent', + $parent_section_id, + $links['collection']['href'] + ), + ); + + // Next. + $next_lesson = $lesson->get_next_lesson(); + if ( $next_lesson ) { + $lesson_links['next'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $next_lesson ) ), + ); + } + + // Previous. + $previous_lesson = $lesson->get_previous_lesson(); + if ( $previous_lesson ) { + $lesson_links['previous'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $previous_lesson ) ), + ); + } + + // Prerequisite. + $prerequisite = $lesson->get_prerequisite(); + + if ( ! empty( $prerequisite ) ) { + $lesson_links['prerequisite'] = array( + 'type' => $this->post_type, + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $prerequisite ) ), + ); + } + + // Quiz. + if ( $lesson->is_quiz_enabled() ) { + $quiz = $lesson->get_quiz(); + $lesson_links['quiz'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'quizzes', $quiz->get( 'id' ) ) ), + ); + } + + $links = array_merge( $links, $lesson_links ); + + /** + * Filters the lesson's links. + * + * @since 1.0.0-beta.7 + * + * @param array $links Links for the given lesson. + * @param LLMS_Lesson $lesson Lesson object. + */ + return apply_filters( 'llms_rest_lesson_links', $links, $lesson ); + + } + + /** + * Checks if a Lesson can be read + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Lesson $lesson The Lesson oject. + * @return bool Whether the post can be read. + * + * @todo Implement read permission based on the section's id: + * 0 indicates an "orphaned" lesson which can be edited and viewed by instructors and admins but cannot be read by students. + */ + protected function check_read_permission( $lesson ) { + + /** + * As of now, lessons of password protected courses cannot be read + */ + if ( post_password_required( $lesson->get_parent_course() ) ) { + return false; + } + + /** + * At the moment we grant lessons read permission only to who can edit lessons. + */ + return parent::check_update_permission( $lesson ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-memberships-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-memberships-controller.php new file mode 100644 index 0000000000..84be3f1b91 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-memberships-controller.php @@ -0,0 +1,677 @@ +<?php +/** + * REST Memberships Controller. + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.9 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Memberships_Controller class. + * + * @since 1.0.0-beta.9 + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Memberships_Controller extends LLMS_REST_Posts_Controller { + + /** + * Enrollments controller. + * + * @var LLMS_REST_Enrollments_Controller + */ + protected $enrollments_controller; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'llms_membership'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'memberships'; + + /** + * Constructor. + * + * @since 1.0.0-beta.9 + */ + public function __construct() { + $this->enrollments_controller = new LLMS_REST_Enrollments_Controller(); + $this->enrollments_controller->set_collection_params( $this->get_enrollments_collection_params() ); + } + + /** + * Retrieves the query params for the enrollments objects collection. + * + * @since 1.0.0-beta.9 + * + * @return array Collection parameters. + */ + public function get_enrollments_collection_params() { + $query_params = $this->enrollments_controller->get_collection_params(); + unset( $query_params['post'] ); + + $query_params['student'] = array( + 'description' => __( + 'Limit results to a specific student or a list of students. Accepts a single student id or a comma separated list of student ids.', + 'lifterlms' + ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Get action/filters to be removed before preparing the item for response. + * + * @since 1.0.0-beta.9 + * + * @param LLMS_Membership $membership Membership object. + * @return array Array of action/filters to be removed for response. + */ + protected function get_filters_to_be_removed_for_response( $membership ) { + + $filters = array(); + + if ( llms_blocks_is_post_migrated( $membership->get( 'id' ) ) ) { + $filters = array( + // hook => [callback, priority]. + 'lifterlms_single_membership_after_summary' => array( + // Membership Information. + array( + 'callback' => 'lifterlms_template_pricing_table', + 'priority' => 10, + ), + ), + ); + } + + /** + * Modify the array of filters to be removed before building the response. + * + * @since 1.0.0-beta.9 + * + * @param array $filters Array of filters to be removed. + * @param LLMS_Membership $membership Membership object. + */ + return apply_filters( 'llms_rest_llms_membership_filters_removed_for_response', $filters, $membership ); + } + + /** + * Get the Membership's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.9 + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['auto_enroll'] = array( + 'description' => __( + 'List of courses to automatically enroll students into when they\'re enrolled into the membership.', + 'lifterlms' + ), + 'type' => 'array', + 'default' => array(), + 'items' => array( + 'type' => 'integer', + ), + ); + + $schema['properties']['catalog_visibility'] = array( + 'description' => __( 'Visibility of the membership in catalogs and search results.', 'lifterlms' ), + 'type' => 'string', + 'enum' => array_keys( llms_get_product_visibility_options() ), + 'default' => 'catalog_search', + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['categories'] = array( + 'description' => __( 'List of membership categories.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['instructors'] = array( + 'description' => __( + 'List of post instructors. Defaults to current user when creating a new post.', + 'lifterlms' + ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'arg_options' => array( + 'validate_callback' => 'llms_validate_instructors', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['restriction_action'] = array( + 'description' => __( + 'Determines the action to take when content restricted by the membership is accessed by a non-member. - `none`: Remain on page and display the message `restriction_message`. - `membership`: Redirect to the membership\'s permalink. - `page`: Redirect to the permalink of the page identified by `restriction_page_id`. - `custom`: Redirect to the URL identified by `restriction_url`.', + 'lifterlms' + ), + 'type' => 'string', + 'default' => 'none', + 'enum' => array( 'none', 'membership', 'page', 'custom' ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['restriction_message'] = array( + 'description' => __( + 'Message to display to non-members after a `restriction_action` redirect. When `restriction_action` is `none` replaces the page content with this message.', + 'lifterlms' + ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). + ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Raw message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'rendered' => array( + 'description' => __( 'Rendered message content.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + 'default' => __( + 'You must belong to the [lifterlms_membership_link id="{{membership_id}}"] membership to access this content.', + 'lifterlms' + ), + ); + + $schema['properties']['restriction_page_id'] = array( + 'description' => __( + 'WordPress page ID used for redirecting non-members when `restriction_action` is `page`.', + 'lifterlms' + ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ); + + $schema['properties']['restriction_url'] = array( + 'description' => __( + 'URL used for redirecting non-members when `restriction_action` is `custom`.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ); + + $schema['properties']['sales_page_page_id'] = array( + 'description' => __( + 'The WordPress page ID of the sales page. Required when `sales_page_type` equals `page`. Only returned when the `sales_page_type` equals `page`.', + 'lifterlms' + ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ); + + $schema['properties']['sales_page_type'] = array( + 'description' => __( + 'Defines alternate content displayed to visitors and non-enrolled students when accessing the post. - `none` displays the post content. - `content` displays alternate content from the `excerpt` property. - `page` redirects to the WordPress page defined in `content_page_id`. - `url` redirects to the URL defined in `content_page_url`.', + 'lifterlms' + ), + 'type' => 'string', + 'default' => 'none', + 'enum' => array_keys( llms_get_sales_page_types() ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['sales_page_url'] = array( + 'description' => __( + 'The URL of the sales page content. Required when `sales_page_type` equals `url`. Only returned when the `sales_page_type` equals `url`.', + 'lifterlms' + ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ); + + $schema['properties']['tags'] = array( + 'description' => __( 'List of membership tags.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ); + + /** + * Filter item schema for the membership controller. + * + * @since 1.0.0-beta.9 + * + * @param array $schema Item schema data. + */ + $schema = apply_filters( 'llms_rest_membership_item_schema', $schema ); + + return $schema; + } + + /** + * Maps a taxonomy name to the relative rest base. + * + * @since 1.0.0-beta.9 + * + * @param object $taxonomy The taxonomy object. + * @return string The taxonomy rest base. + */ + protected function get_taxonomy_rest_base( $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + $taxonomy_base_map = array( + 'membership_cat' => 'categories', + 'membership_tag' => 'tags', + ); + + return isset( $taxonomy_base_map[ $base ] ) ? $taxonomy_base_map[ $base ] : $base; + } + + /** + * Prepares a single post for create or update. + * + * @since 1.0.0-beta.9 + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of llms post args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + $prepared_item = parent::prepare_item_for_database( $request ); + $schema = $this->get_item_schema(); + + // Restriction action. + if ( ! empty( $schema['properties']['restriction_action'] ) && isset( $request['restriction_action'] ) ) { + $prepared_item['restriction_redirect_type'] = $request['restriction_action']; + } + + // Restriction page id. + if ( ! empty( $schema['properties']['restriction_page_id'] ) && isset( $request['restriction_page_id'] ) ) { + $page = get_post( $request['restriction_page_id'] ); + if ( $page && is_a( $page, 'WP_Post' ) ) { + $prepared_item['redirect_page_id'] = $request['restriction_page_id']; + } else { + $prepared_item['redirect_page_id'] = 0; + } + } + + // Restriction URL. + if ( ! empty( $schema['properties']['restriction_url'] ) && isset( $request['restriction_url'] ) ) { + $prepared_item['redirect_custom_url'] = $request['restriction_url']; + } + + // Sales page id. + if ( ! empty( $schema['properties']['sales_page_page_id'] ) && isset( $request['sales_page_page_id'] ) ) { + $page = get_post( $request['sales_page_page_id'] ); + if ( $page && is_a( $page, 'WP_Post' ) ) { + $prepared_item['sales_page_content_page_id'] = $request['sales_page_page_id']; + } else { + $prepared_item['sales_page_content_page_id'] = 0; + } + } + + // Sales page type. + if ( ! empty( $schema['properties']['sales_page_type'] ) && isset( $request['sales_page_type'] ) ) { + $prepared_item['sales_page_content_type'] = $request['sales_page_type']; + } + + // Sales page URL. + if ( ! empty( $schema['properties']['sales_page_url'] ) && isset( $request['sales_page_url'] ) ) { + $prepared_item['sales_page_content_url'] = $request['sales_page_url']; + } + + /** + * Filters the membership data for a response. + * + * @since 1.0.0-beta.9 + * + * @param array $prepared_item Array of membership item properties prepared for database. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + */ + $prepared_item = apply_filters( 'llms_rest_pre_insert_membership', $prepared_item, $request, $schema ); + + return $prepared_item; + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.9 + * @since 1.0.0-beta.14 Added `$request` parameter. + * @since 1.0.0-beta.18 Fixed access plans link. + * + * @param LLMS_Membership $membership LLMS Membership. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $membership, $request ) { + + $links = parent::prepare_links( $membership, $request ); + unset( $links['content'] ); + $id = $membership->get( 'id' ); + + // Access plans. + $links['access_plans'] = array( + 'href' => add_query_arg( + 'post_id', + $id, + rest_url( sprintf( '%s/%s', $this->namespace, 'access-plans' ) ) + ), + ); + + // Auto enrollment courses. + $auto_enroll_courses = implode( ',', $membership->get_auto_enroll_courses() ); + if ( $auto_enroll_courses ) { + $links['auto_enrollment_courses'] = array( + 'href' => add_query_arg( + 'include', + $auto_enroll_courses, + rest_url( sprintf( '%s/%s', $this->namespace, 'courses' ) ) + ), + ); + } + + // Enrollments. + $links['enrollments'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d/%s', $this->namespace, $this->rest_base, $id, 'enrollments' ) ), + ); + + // Instructors. + $links['instructors'] = array( + 'href' => add_query_arg( + 'post', + $id, + rest_url( sprintf( '%s/%s', $this->namespace, 'instructors' ) ) + ), + ); + + // Students. + $links['students'] = array( + 'href' => add_query_arg( + 'enrolled_in', + $id, + rest_url( sprintf( '%s/%s', $this->namespace, 'students' ) ) + ), + ); + + /** + * Filters the membership's links. + * + * @since 1.0.0-beta.9 + * + * @param array $links Links for the given membership. + * @param LLMS_Membership $membership LLMS Membership object. + */ + $links = apply_filters( 'llms_rest_membership_links', $links, $membership ); + + return $links; + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.9 + * + * @param LLMS_Membership $membership Membership object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $membership, $request ) { + $data = parent::prepare_object_for_response( $membership, $request ); + $context = $request->get_param( 'context' ); + + // Auto enroll. + $data['auto_enroll'] = $membership->get_auto_enroll_courses(); + + // Catalog visibility. + $data['catalog_visibility'] = $membership->get_product()->get_catalog_visibility(); + + // Categories. + $data['categories'] = $membership->get_categories( + array( + 'fields' => 'ids', + ) + ); + + // Instructors. + $instructors = $membership->get_instructors(); + $instructors = empty( $instructors ) ? array() : wp_list_pluck( $instructors, 'id' ); + $data['instructors'] = $instructors; + + // Restriction action. + $data['restriction_action'] = $membership->get( 'restriction_redirect_type' ); + $data['restriction_action'] = $data['restriction_action'] ? $data['restriction_action'] : 'none'; + + // Restriction message. + $data['restriction_message'] = array( + 'raw' => $membership->get( 'restriction_notice', $raw = true ), + 'rendered' => do_shortcode( $membership->get( 'restriction_notice' ) ), + ); + + // Restriction page id. + if ( 'page' === $data['restriction_action'] || 'edit' === $context ) { + $data['restriction_page_id'] = $membership->get( 'redirect_page_id' ); + } + + // Restriction URL. + if ( 'custom' === $data['restriction_action'] || 'edit' === $context ) { + $data['restriction_url'] = $membership->get( 'redirect_custom_url' ); + } + + // Tags. + $data['tags'] = $membership->get_tags( + array( + 'fields' => 'ids', + ) + ); + + // Sales page type. + $data['sales_page_type'] = $membership->get( 'sales_page_content_type' ); + $data['sales_page_type'] = $data['sales_page_type'] ? $data['sales_page_type'] : 'none'; + + // Sales page id. + if ( 'page' === $data['sales_page_type'] || 'edit' === $context ) { + $data['sales_page_page_id'] = $membership->get( 'sales_page_content_page_id' ); + } + + // Sales page url. + if ( 'custom' === $data['sales_page_type'] || 'edit' === $context ) { + $data['sales_page_url'] = $membership->get( 'sales_page_content_url' ); + } + + /** + * Filters the membership data for a response. + * + * @since 1.0.0-beta.9 + * + * @param array $data Array of lesson properties prepared for response. + * @param LLMS_Membership $membership Membership object. + * @param WP_REST_Request $request Full details about the request. + */ + $data = apply_filters( 'llms_rest_prepare_membership_object_response', $data, $membership, $request ); + + return $data; + } + + /** + * Register routes. + * + * @since 1.0.0-beta.9 + * + * @return void + */ + public function register_routes() { + parent::register_routes(); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)/enrollments', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique Membership Identifier. The WordPress Post ID', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this->enrollments_controller, 'get_items' ), + 'permission_callback' => array( $this->enrollments_controller, 'get_items_permissions_check' ), + 'args' => $this->enrollments_controller->get_collection_params(), + ), + 'schema' => array( $this->enrollments_controller, 'get_public_item_schema' ), + ) + ); + } + + /** + * Updates an existing single LLMS_Membership in the database. + * + * This method should be used for membership properties that require the membership id in order to be saved in the database. + * + * @since 1.0.0-beta.9 + * + * @param LLMS_Membership $membership LLMS_Membership instance. + * @param WP_REST_Request $request Full details about the request. + * @param array $schema The item schema. + * @param array $prepared_item Array. + * @param bool $creating Optional. Whether we're in creation or update phase. Default true (create). + * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update. + */ + protected function update_additional_object_fields( $membership, $request, $schema, $prepared_item, $creating = true ) { + $error = new WP_Error(); + + // Auto enroll. + if ( ! empty( $schema['properties']['auto_enroll'] ) && isset( $request['auto_enroll'] ) ) { + $membership->add_auto_enroll_courses( $request['auto_enroll'], true ); + } + + // Catalog visibility. + if ( ! empty( $schema['properties']['catalog_visibility'] ) && isset( $request['catalog_visibility'] ) ) { + $membership->get_product()->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Instructors. + if ( ! empty( $schema['properties']['instructors'] ) ) { + + $instructors = array(); + + if ( isset( $request['instructors'] ) ) { + foreach ( $request['instructors'] as $instructor_id ) { + $user_data = get_userdata( $instructor_id ); + if ( ! empty( $user_data ) ) { + $instructors[] = array( + 'id' => $instructor_id, + 'name' => $user_data->display_name, + ); + } + } + } + + // When creating, always make sure the instructors are set. + // Note: `$membership->set_instructor( $instructors )` when `$instructors` is empty + // will set the membership's author as membership's instructor. + if ( $membership || ( ! $creating && isset( $request['instructors'] ) ) ) { + $membership->set_instructors( $instructors ); + } + } + + $to_set = array(); + + /** + * The following properties have a default value that contains a placeholder, e.g. `{{membership_id}}`, + * that can be "expanded" only after the membership has been created. + */ + // Restriction message. + if ( ! empty( $schema['properties']['restriction_message'] ) ) { + if ( isset( $request['restriction_message'] ) ) { + if ( is_string( $request['restriction_message'] ) ) { + $to_set['restriction_notice'] = $request['restriction_message']; + } elseif ( isset( $request['restriction_message']['raw'] ) ) { + $to_set['restriction_notice'] = $request['restriction_message']['raw']; + } + } elseif ( $creating ) { + $to_set['restriction_notice'] = $schema['properties']['restriction_message']['properties']['raw']['default']; + } + } + + // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + // On creation, since the restriction message has a non empty default, the restriction_add_notice, + // will be set to 'yes'. + $to_set['restriction_add_notice'] = empty( $to_set['restriction_notice'] ) ? 'no' : 'yes'; + + // Are we creating a membership? TODO what about updating when message is empty? + // If so, replace the placeholder with the actual membership id. + if ( $creating ) { + $_to_expand_props = array( + 'restriction_notice', + ); + $membership_id = $membership->get( 'id' ); + foreach ( $_to_expand_props as $prop ) { + if ( ! empty( $to_set[ $prop ] ) ) { + $to_set[ $prop ] = str_replace( '{{membership_id}}', $membership_id, $to_set[ $prop ] ); + } + } + } else { // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908. + $_props = array( + 'restriction_add_notice', + ); + foreach ( $_props as $_prop ) { + if ( isset( $to_set[ $_prop ] ) && $to_set[ $_prop ] === $membership->get( $_prop ) ) { + unset( $to_set[ $_prop ] ); + } + } + } + + // Set bulk. + if ( ! empty( $to_set ) ) { + $update = $membership->set_bulk( $to_set, true ); + if ( is_wp_error( $update ) ) { + $error = $update; + } + } + + if ( $error->errors ) { + return $error; + } + + return ! empty( $to_set ); + } +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-sections-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-sections-controller.php new file mode 100644 index 0000000000..014dda558e --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-sections-controller.php @@ -0,0 +1,547 @@ +<?php +/** + * REST Sections Controller + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.21 + */ + +defined( 'ABSPATH' ) || exit; + + +/** + * LLMS_REST_Sections_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 `prepare_objects_query()` renamed to `prepare_collection_query_args()`. + * Fix the way we get the section's parent course object when building the resource links. + * @since 1.0.0-beta.9 Removed `create_llms_post()` and `get_object()` methods, now abstracted in `LLMS_REST_Posts_Controller` class. + * @since 1.0.0-beta.12 Updated `$this->prepare_collection_query_args()` to reflect changes in the parent class. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Sections_Controller extends LLMS_REST_Posts_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'sections'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'section'; + + /** + * Parent id. + * + * @var int + */ + protected $parent_id; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'title', + 'date_created', + 'date_updated', + 'order', + 'relevance', + ); + + /** + * Lessons controller class. + * + * @var string + */ + protected $content_controller_class; + + /** + * Lessons controller. + * + * @var LLMS_REST_Lessons_Controller + */ + protected $content_controller; + + /** + * Constructor. + * + * @since 1.0.0-beta.1 + * + * @param string $content_controller_class Optional. The class name of the content controller. Default 'LLMS_REST_Lessons_Controller'. + */ + public function __construct( $content_controller_class = 'LLMS_REST_Lessons_Controller' ) { + + $this->collection_params = $this->build_collection_params(); + $this->content_controller_class = $content_controller_class; + + if ( $this->content_controller_class && class_exists( $this->content_controller_class ) ) { + $this->content_controller = new $this->content_controller_class(); + $this->content_controller->set_collection_params( $this->get_content_collection_params() ); + } + + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function register_routes() { + + parent::register_routes(); + + if ( isset( $this->content_controller ) ) { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)/content', + array( + 'args' => array( + 'id' => array( + // translators: %1$s the post type name. + 'description' => sprintf( __( 'Unique %1$s Identifier. The WordPress Post ID', 'lifterlms' ), $this->post_type ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_content_items' ), + 'permission_callback' => array( $this->content_controller, 'get_items_permissions_check' ), + 'args' => $this->content_controller->get_collection_params(), + ), + 'schema' => array( $this->content_controller, 'get_public_item_schema' ), + ) + ); + } + } + + /** + * Retrieves an array of arguments for the delete endpoint. + * + * @since 1.0.0-beta.1 + * + * @return array Delete endpoint arguments. + */ + public function get_delete_item_args() { + return array(); + } + + /** + * Whether the delete should be forced. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return bool True if the delete should be forced, false otherwise. + */ + protected function is_delete_forced( $request ) { + return true; + } + + /** + * Whether the trash is supported. + * + * @since 1.0.0-beta.1 + * + * @return bool True if the trash is supported, false otherwise. + */ + protected function is_trash_supported() { + return false; + } + + /** + * Set parent id. + * + * @since 1.0.0-beta.1 + * + * @param int $parent_id Course parent id. + * @return void + */ + public function set_parent_id( $parent_id ) { + $this->parent_id = $parent_id; + } + + /** + * Get parent id. + * + * @since 1.0.0-beta.1 + * + * @return int|null Course parent id. Null if not set. + */ + public function get_parent_id() { + return isset( $this->parent_id ) ? $this->parent_id : null; + } + + /** + * Prepares a single post for create or update. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error Array of llms post args or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + + $prepared_item = parent::prepare_item_for_database( $request ); + + $schema = $this->get_item_schema(); + + // LLMS Section parent id. + if ( ! empty( $schema['properties']['parent_id'] ) && isset( $request['parent_id'] ) ) { + + $parent_course = llms_get_post( $request['parent_id'] ); + + if ( ! $parent_course || ! is_a( $parent_course, 'LLMS_Course' ) ) { + return llms_rest_bad_request_error( __( 'Invalid parent_id param. It must be a valid Course ID.', 'lifterlms' ) ); + } + + $prepared_item['parent_course'] = $request['parent_id']; + } + + // LLMS Section order. + if ( ! empty( $schema['properties']['order'] ) && isset( $request['order'] ) ) { + + // order must be > 0. It's sanitized as absint so it cannot come as negative value. + if ( 0 === $request['order'] ) { + return llms_rest_bad_request_error( __( 'Invalid order param. It must be greater than 0.', 'lifterlms' ) ); + } + + $prepared_item['order'] = $request['order']; + } + + return $prepared_item; + + } + + /** + * Get the Section's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_item_schema() { + + $schema = parent::get_item_schema(); + + // Section's title. + $schema['properties']['title']['description'] = __( 'Section Title', 'lifterlms' ); + + // Section's parent id. + $schema['properties']['parent_id'] = array( + 'description' => __( 'WordPress post ID of the parent item. Must be a Course ID.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + 'required' => true, + ); + + // Section order. + $schema['properties']['order'] = array( + 'description' => __( 'Order of the section within the course.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + 'required' => true, + ); + + // remove unnecessary properties. + $unnecessary_properties = array( + 'permalink', + 'slug', + 'content', + 'menu_order', + 'excerpt', + 'featured_media', + 'status', + 'password', + 'featured_media', + 'comment_status', + 'ping_status', + ); + + foreach ( $unnecessary_properties as $unnecessary_property ) { + unset( $schema['properties'][ $unnecessary_property ] ); + } + + return $schema; + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array The Enrollments collection parameters. + */ + public function get_collection_params() { + return $this->collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @param array $collection_params The Enrollments collection parameters to be set. + * @return void + */ + public function set_collection_params( $collection_params ) { + $this->collection_params = $collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function build_collection_params() { + + $query_params = parent::get_collection_params(); + + $query_params['parent'] = array( + 'description' => __( 'Filter sections by the parent post (course) ID.', 'lifterlms' ), + 'type' => 'integer', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Prepare a single object output for response. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Section $section Section object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $section, $request ) { + + $data = parent::prepare_object_for_response( $section, $request ); + + // Parent course. + $data['parent_id'] = $section->get_parent_course(); + + // Order. + $data['order'] = $section->get( 'order' ); + + return $data; + + } + + /** + * Format query arguments to retrieve a collection of objects. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.12 Updated to reflect changes in the parent class. + * @since 1.0.0-beta.18 Correctly return errors. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $query_args = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $query_args ) ) { + return $query_args; + } + + // Orderby 'order' requires a meta query. + if ( isset( $query_args['orderby'] ) && 'order' === $query_args['orderby'] ) { + $query_args = array_merge( + $query_args, + array( + 'meta_key' => '_llms_order', + 'orderby' => 'meta_value_num', + ) + ); + } + + if ( isset( $this->parent_id ) ) { + $parent_id = $this->parent_id; + } elseif ( ! empty( $request['parent'] ) && $request['parent'] > 1 ) { + $parent_id = $request['parent']; + } + + // Filter by parent. + if ( ! empty( $parent_id ) ) { + $query_args = array_merge( + $query_args, + array( + 'meta_query' => array( + array( + 'key' => '_llms_parent_course', + 'value' => $parent_id, + 'compare' => '=', + ), + ), + ) + ); + } + + return $query_args; + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Fix the way we get the section's parent course object. + * @since 1.0.0-beta.14 Added `$request` parameter. + * + * @param LLMS_Section $section LLMS Section. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $section, $request ) { + + $links = parent::prepare_links( $section, $request ); + $parent_course_id = $section->get_parent_course(); + + // If the section has no course parent return earlier. + if ( ! $parent_course_id ) { + return $links; + } + + $parent_course = llms_get_post( $parent_course_id ); + if ( ! is_a( $parent_course, 'LLMS_Course' ) ) { + return $links; + } + + $section_id = $section->get( 'id' ); + $section_links = array(); + + // Parent (course). + $section_links['parent'] = array( + 'type' => 'course', + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $parent_course_id ) ), + ); + + // Siblings. + $section_links['siblings'] = array( + 'href' => add_query_arg( + 'parent', + $parent_course_id, + $links['collection']['href'] + ), + ); + + // Next. + $next_section = $section->get_next(); + if ( $next_section ) { + $section_links['next'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $next_section->get( 'id' ) ) ), + ); + } + + // Previous. + $previous_section = $section->get_previous(); + if ( $previous_section ) { + $section_links['previous'] = array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $previous_section->get( 'id' ) ) ), + ); + } + + return array_merge( $links, $section_links ); + } + + /** + * Checks if a Section can be read + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Section $section The Section oject. + * @return bool Whether the post can be read. + */ + protected function check_read_permission( $section ) { + + /** + * As of now, sections of password protected courses cannot be read + */ + if ( post_password_required( $section->get( 'parent_course' ) ) ) { + return false; + } + + return parent::check_read_permission( $section ); + + } + + /** + * Retrieves the content controller. + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_Lessons_Controller|null + */ + public function get_content_controller() { + return $this->content_controller; + } + + /** + * Retrieves the query params for the lessons objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_content_collection_params() { + + $query_params = $this->content_controller->get_collection_params(); + + $query_params['orderby']['enum'] = array( + 'order', + 'id', + 'title', + ); + $query_params['orderby']['default'] = 'order'; + + unset( $query_params['parent'] ); + + return $query_params; + + } + + /** + * Get a collection of content items (lessons). + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_content_items( $request ) { + + $this->content_controller->set_parent_id( $request['id'] ); + $result = $this->content_controller->get_items( $request ); + + // Specs require 404 when no section's lessons are found. + if ( ! is_wp_error( $result ) && empty( $result->data ) ) { + return llms_rest_not_found_error(); + } + + return $result; + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-students-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-students-controller.php new file mode 100644 index 0000000000..73831f413c --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-students-controller.php @@ -0,0 +1,393 @@ +<?php +/** + * REST Resource Controller for Students + * + * @package LifterLMS_REST/Classes/Controllers + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Students_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Added `prepare_args_for_total_count_query()` method override. + * @since 1.0.0-beta.12 Added item schema filter. + * Added 'llms_rest_student_registered' action hook - fired after student's creation. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Students_Controller extends LLMS_REST_Users_Controller { + + /** + * Resource ID or Name. + * + * @var string + */ + protected $resource_name = 'student'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'students'; + + /** + * Temporary array of prepared query args used to filter WP_User_Query + * when `enrolled_in` and `enrolled_not_in` args are present on the request. + * + * @var array + */ + private $prepared_query_args = array(); + + /** + * Determine if the current user can view the requested student. + * + * @since 1.0.0-beta.1 + * + * @param int $item_id WP_User id. + * @return bool + */ + protected function check_read_item_permissions( $item_id ) { + + if ( get_current_user_id() === $item_id ) { + return true; + } + + return current_user_can( 'view_students', $item_id ); + + } + + /** + * Determine if current user has permission to create a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function create_item_permissions_check( $request ) { + + if ( ! current_user_can( 'create_students' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to create new students.', 'lifterlms' ) ); + } + + return $this->check_roles_permissions( $request ); + + } + + /** + * Determine if current user has permission to delete a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + if ( ! current_user_can( 'delete_students', $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to delete this student.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $params = parent::get_collection_params(); + + // $params['roles']['default'] = 'student'; + + $params['enrolled_in'] = array( + 'description' => __( 'Retrieve only students enrolled in the specified course(s) and/or membership(s). Accepts a single WP Post ID or a comma separated list of IDs.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + + $params['enrolled_not_in'] = array( + 'description' => __( 'Retrieve only students not enrolled in the specified course(s) and/or membership(s). Accepts a single WP Post ID or a comma separated list of IDs.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + + return $params; + + } + + /** + * Get the item schema. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Added item schema filter. + * + * @return array + */ + public function get_item_schema() { + + $schema = parent::get_item_schema(); + $schema['properties']['roles']['default'] = array( 'student' ); + + /** + * Filter item schema for the studend controller. + * + * @since 1.0.0-beta.12 + * + * @param array $schema Item schema data. + */ + return apply_filters( 'llms_rest_student_item_schema', $schema ); + + } + + /** + * Determine if current user has permission to get a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function get_item_permissions_check( $request ) { + + if ( ! $this->check_read_item_permissions( $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to view this student.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Determine if current user has permission to list users. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function get_items_permissions_check( $request ) { + + if ( ! empty( $request['roles'] ) && ! current_user_can( 'view_others_students' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to filter students by role.', 'lifterlms' ) ); + } + + if ( ! current_user_can( 'view_students' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to list students.', 'lifterlms' ) ); + } + + return true; + + } + + /** + * Get object. + * + * @since 1.0.0-beta.1 + * + * @param int $id Object ID. + * @return LLMS_Student|WP_Error + */ + protected function get_object( $id ) { + + $student = llms_get_student( $id ); + return $student ? $student : llms_rest_not_found_error(); + + } + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return WP_User_Query + */ + protected function get_objects_query( $prepared, $request ) { + + $remove = false; + if ( ! empty( $prepared['enrolled_in'] ) || ! empty( $prepared['enrolled_not_in'] ) ) { + + $this->prepared_query_args = $prepared; + add_action( 'pre_user_query', array( $this, 'get_objects_query_pre' ) ); + $remove = true; + + } + + $query = parent::get_objects_query( $prepared, $request ); + + if ( $remove ) { + + $this->prepared_query_args = array(); + + remove_action( 'pre_user_query', array( $this, 'get_objects_query_pre' ) ); + } + + return $query; + + } + + /** + * Callback for WP_User_Query "pre_user_query" action. + * + * Adds select fields and a having clause to check against `enrolled_in` and `enrolled_not_in` collection query args. + * + * @since 1.0.0-beta.1 + * + * @link https://developer.wordpress.org/reference/hooks/pre_user_query/ + * + * @param WP_User_Query $query Query object. + * @return void + */ + public function get_objects_query_pre( $query ) { + + $query->query_where .= ' Having 1 '; + + if ( ! empty( $this->prepared_query_args['enrolled_in'] ) ) { + foreach ( $this->prepared_query_args['enrolled_in'] as $post_id ) { + $post_id = absint( $post_id ); + $query->query_fields .= ", {$this->get_objects_query_status_subquery( $post_id )}"; + $query->query_where .= " AND p_{$post_id}_stat = 'enrolled'"; + } + } + + if ( ! empty( $this->prepared_query_args['enrolled_not_in'] ) ) { + foreach ( $this->prepared_query_args['enrolled_not_in'] as $post_id ) { + $post_id = absint( $post_id ); + $query->query_fields .= ", {$this->get_objects_query_status_subquery( $post_id )}"; + $query->query_where .= " AND ( p_{$post_id}_stat IS NULL OR p_{$post_id}_stat != 'enrolled' )"; + } + } + + } + + /** + * Generates a subquery to check a user's enrollment status for a given course or membership. + * + * @since 1.0.0-beta.1 + * + * @param int $post_id Course or membership id. + * @return string + */ + private function get_objects_query_status_subquery( $post_id ) { + + global $wpdb; + + return "( + SELECT meta_value + FROM {$wpdb->prefix}lifterlms_user_postmeta + WHERE user_id = {$wpdb->users}.ID + AND post_id = {$post_id} + AND meta_key = '_status' + ORDER BY updated_date DESC + LIMIT 1 + ) AS p_{$post_id}_stat"; + + } + + /** + * Prepare query args for total count query. + * + * @since 1.0.0-beta.7 + * + * @param array $args Array of query args. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_args_for_total_count_query( $args, $request ) { + // run the query again on page one to get a proper total count. + $args['page'] = 1; + return $args; + } + + /** + * Called right after a student is completely inserted (created/updated). + * + * @since 1.0.0-beta.12 + * + * @param LLMS_Student $student Inserted or updated llms student. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + * @param bool $creating True when creating a post, false when updating. + */ + protected function object_completely_inserted( $student, $request, $schema, $creating ) { + + parent::object_completely_inserted( $student, $request, $schema, $creating ); + + if ( $creating ) { + /** + * Fires after a LifterLMS student has been created via the REST API. + * + * @since 1.0.0-beta.12 + * + * @param LLMS_Student $student Inserted or updated llms student. + * @param WP_REST_Request $request Request object. + * @param array $schema The item schema. + */ + do_action( 'llms_rest_student_registered', $this->get_object_id( $student ), $request, $schema ); + } + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added `$request` parameter. + * + * @param obj $object Item object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $object, $request ) { + + $links = parent::prepare_links( $object, $request ); + + $links['enrollments'] = array( + 'href' => sprintf( '%s/enrollments', $links['self']['href'] ), + ); + $links['progress'] = array( + 'href' => sprintf( '%s/progress', $links['self']['href'] ), + ); + + return $links; + + } + + /** + * Determine if current user has permission to update a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function update_item_permissions_check( $request ) { + + if ( get_current_user_id() === $request['id'] ) { + return true; + } + + if ( ! current_user_can( 'edit_students', $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to edit this student.', 'lifterlms' ) ); + } + + return $this->check_roles_permissions( $request ); + + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-students-progress-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-students-progress-controller.php new file mode 100644 index 0000000000..e3a4513dbf --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-students-progress-controller.php @@ -0,0 +1,541 @@ +<?php +/** + * REST Controller for Student Progress. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.14 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Student_Progress_Controller class. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.13 Fixed student/lesson post meta key to delete when deleting a student progress. + * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`. + */ +class LLMS_REST_Students_Progress_Controller extends LLMS_REST_Controller { + + /** + * Base Resource + * + * @var string + */ + protected $rest_base = 'students/(?P<id>[\d]+)/progress/(?P<post_id>[\d]+)'; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'date_created', + 'date_updated', + 'progress', + ); + + /** + * Determine if the current user can view the requested item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return bool + */ + protected function check_read_item_permissions( $request ) { + + // Can read your own progress. + if ( get_current_user_id() === $request['id'] ) { + return true; + } + + // Must be able to edit post and student to view other's progress. + if ( current_user_can( 'edit_post', $request['post_id'] ) && current_user_can( 'edit_students', $request['id'] ) ) { + return true; + } + + return false; + + } + + /** + * Determine if current user has permission to delete a user. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + if ( ! current_user_can( 'edit_post', $request['post_id'] ) || ! current_user_can( 'delete_students', $request['id'] ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + + } + + /** + * Delete the object. + * + * Note: we do not return 404s when the resource to delete cannot be found. We assume it's already been deleted and respond with 204. + * Errors returned by this method should be any error other than a 404! + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.13 Fixed student/lesson post meta key to delete, `_is_complete` in place of `_status`. + * + * @param obj $object Instance of the object from $this->get_object(). + * @param WP_REST_Request $request Request object. + * @return true|WP_Error true when the object is removed, WP_Error on failure. + */ + protected function delete_object( $object, $request ) { + + $post = llms_get_post( $request['post_id'] ); + + $ids = 'lesson' === $post->get( 'type' ) ? array( $post->get( 'id' ) ) : $post->get_lessons( 'ids' ); + + if ( $ids ) { + foreach ( $ids as $id ) { + llms_bulk_delete_user_postmeta( + $request['id'], + $id, + array( + '_is_complete' => null, + '_completion_trigger' => null, + ) + ); + + } + } + + if ( 'lesson' !== $post->get( 'type' ) ) { + llms_mark_incomplete( $request['id'], $post->get( 'id' ), $post->get( 'type' ) ); + } + + return true; + + } + + /** + * Retrieve a updated/created dates for a given post. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Student $student Student Object. + * @param LLMS_Course|LLMS_Section|LLMS_Lesson $post Course, Section, or Lesson post object. + * @param string $order Sort order, ASC or DESC. + * @return string|null + */ + protected function get_date( $student, $post, $order ) { + + $lessons = 'lesson' === $post->get( 'type' ) ? array( $post->get( 'id' ) ) : $post->get_lessons( 'ids' ); + + if ( $lessons ) { + + $lessons = implode( ', ', $lessons ); + + global $wpdb; + // @todo: rewrite query so we don't have to ignore CS rules. + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $date = $wpdb->get_var( + $wpdb->prepare( + " + SELECT updated_date + FROM {$wpdb->prefix}lifterlms_user_postmeta + WHERE user_id = %d + AND post_id IN ( {$lessons} ) + AND meta_key = '_is_complete' + ORDER BY updated_date {$order} + LIMIT 1; + ", + $student->get( 'id' ) + ) + ); + //phpcs:enable + + if ( $date ) { + return mysql_to_rfc3339( $date ); + } + } + + return null; + + } + + /** + * Get a single item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + + $object = $this->get_object( array( $request['id'], $request['post_id'] ) ); + $response = $this->prepare_item_for_response( $object, $request ); + + return rest_ensure_response( $response ); + + } + + /** + * Check if a given request has access to read an item. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + + if ( ! $this->check_read_item_permissions( $request ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + } + + /** + * Get the API Key's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.1 + * + * @return array + */ + public function get_item_schema() { + + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'students-progress', + 'type' => 'object', + 'properties' => array( + 'student_id' => array( + 'description' => __( 'The ID of the student.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'post_id' => array( + 'description' => __( 'The ID of the course/membership.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'Creation date. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => array( $this, 'validate_date_created' ), + ), + ), + 'date_updated' => array( + 'description' => __( 'Date last modified. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'The status of the enrollment.', 'lifterlms' ), + 'enum' => array( 'complete', 'incomplete' ), + 'context' => array( 'view', 'edit' ), + 'type' => 'string', + 'required' => true, + ), + 'progress' => array( + 'description' => __( 'Student\'s progress as a percentage.', 'lifterlms' ), + 'enum' => array( 'complete', 'incomplete' ), + 'context' => array( 'view', 'edit' ), + 'type' => 'number', + 'readonly' => true, + ), + ), + ); + + } + + /** + * Get object. + * + * @since 1.0.0-beta.1 + * + * @param int[] $ids { + * Numeric array of ids. + * + * @type int $ids[0] Student id. + * @type int $ids[1] Post id. + * } + * @return object|WP_Error + */ + protected function get_object( $ids ) { + + $obj = new stdClass(); + if ( ! is_array( $ids ) ) { + return $obj; + } + + $student_id = $ids[0]; + $post_id = $ids[1]; + + $post = llms_get_post( $post_id ); + + $student = llms_get_student( $student_id ); + + $obj->student_id = $student_id; + $obj->post_id = $post_id; + + if ( 'lesson' === $post->get( 'type' ) ) { + $obj->progress = $student->is_complete( $post_id, 'lesson' ) ? (float) 100 : (float) 0; + } else { + $obj->progress = (float) $student->get_progress( $post_id, $post->get( 'type' ) ); + } + + $obj->status = $obj->progress < 100 ? 'incomplete' : 'complete'; + + $obj->date_updated = $this->get_date( $student, $post, 'DESC' ); + $obj->date_created = $this->get_date( $student, $post, 'ASC' ); + + return $obj; + + } + + /** + * Retrieve an ID from the object + * + * @since 1.0.0-beta.1 + * + * @param obj $object Item object. + * @return int + */ + protected function get_object_id( $object ) { + + return array( $object->student_id, $object->post_id ); + + } + + + /** + * Prepare request arguments for a database insert/update. + * + * @since 1.0.0-beta.1 + * + * @param WP_Rest_Request $request Request object. + * @return array + */ + protected function prepare_item_for_database( $request ) { + + $prepared = parent::prepare_item_for_database( $request ); + $prepared['id'] = $request['id']; + + return $prepared; + + } + + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.14 Added `$request` parameter. + * + * @param obj $object Item object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $object, $request ) { + + $base = rest_url( + sprintf( + '/%1$s/%2$s', + $this->namespace, + str_replace( + array( '(?P<id>[\d]+)', '(?P<post_id>[\d]+)' ), + array( $object->student_id, $object->post_id ), + $this->rest_base + ) + ) + ); + + $post_type = get_post_type( $object->post_id ); + + $links = array( + 'self' => array( + 'href' => $base, + ), + 'post' => array( + 'type' => $post_type, + 'href' => rest_url( sprintf( '/%1$s/%2$ss/%3$d', $this->namespace, $post_type, $object->post_id ) ), + ), + 'student' => array( + 'href' => rest_url( sprintf( '/%1$s/students/%2$d', $this->namespace, $object->student_id ) ), + ), + ); + + return $links; + + } + + /** + * Prepare an object for response. + * + * @since 1.0.0-beta.1 + * + * @param LLMS_Abstract_User_Data $object User object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_object_for_response( $object, $request ) { + + return (array) $object; + + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the student. The WP User ID.', 'lifterlms' ), + 'type' => 'integer', + ), + 'post_id' => array( + 'description' => __( 'Unique course, lesson, or section Identifer. The WordPress Post ID.', 'lifterlms' ), + 'type' => 'integer', + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + array( + 'methods' => 'POST', + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( 'POST' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + } + + /** + * Determine if current user has permission to create/update an enrollment. + * + * @since 1.0.0-beta.1 + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function update_item_permissions_check( $request ) { + + if ( ! current_user_can( 'edit_post', $request['post_id'] ) || ! current_user_can( 'edit_students', $request['id'] ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + + } + + /** + * Update the object in the database with prepared data. + * + * @since 1.0.0-beta.1 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from $this->get_object(). + */ + protected function update_object( $prepared, $request ) { + + if ( in_array( get_post_type( $prepared['post_id'] ), array( 'course', 'section' ), true ) ) { + $post = llms_get_post( $prepared['post_id'] ); + $lessons = $post->get_lessons( 'ids' ); + } else { + $lessons = array( $prepared['post_id'] ); + } + + foreach ( $lessons as $lesson_id ) { + + if ( 'complete' === $prepared['status'] ) { + llms_mark_complete( $prepared['id'], $lesson_id, 'lesson', 'api_' . get_current_user_id() ); + } elseif ( 'incomplete' === $prepared['status'] ) { + llms_mark_incomplete( $prepared['id'], $lesson_id, 'lesson', 'api_' . get_current_user_id() ); + } + } + + return $this->get_object( array( $prepared['id'], $prepared['post_id'] ) ); + + } + + /** + * Validate the date_created + * + * @since 1.0.0-beta.1 + * + * @param string $value Date string. + * @param WP_REST_Request $request Request object. + * @param string $param Parameter name ("post_id"). + * @return bool + */ + public function validate_date_created( $value, $request, $param ) { + + $ts = rest_parse_date( $value ); + $now = llms_current_time( 'U' ); + + if ( $ts > $now ) { + return llms_rest_bad_request_error( __( 'Created date cannot be in the future.', 'lifterlms' ) ); + } + + return true; + } + + /** + * Validate the path parameter "post_id". + * + * @since 1.0.0-beta.1 + * + * @param int $value Post ID. + * @param WP_REST_Request $request Request object. + * @param string $param Parameter name ("post_id"). + * @return bool + */ + public function validate_post_id( $value, $request, $param ) { + $post = get_post( $value ); + if ( ! $post ) { + return false; + } elseif ( ! in_array( $post->post_type, array( 'course', 'lesson', 'section' ), true ) ) { + return false; + } elseif ( ! llms_is_user_enrolled( $request['id'], $value ) ) { + return false; + } + + return true; + } + +} diff --git a/libraries/lifterlms-rest/includes/server/class-llms-rest-webhooks-controller.php b/libraries/lifterlms-rest/includes/server/class-llms-rest-webhooks-controller.php new file mode 100644 index 0000000000..854c9299fd --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/class-llms-rest-webhooks-controller.php @@ -0,0 +1,399 @@ +<?php +/** + * REST Controller for Webhooks. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.3 + * @version 1.0.0-beta.3 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Webhooks_Controller class. + * + * @since 1.0.0-beta.3 + */ +class LLMS_REST_Webhooks_Controller extends LLMS_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'webhooks'; + + /** + * Schema properties available for ordering the collection. + * + * @var string[] + */ + protected $orderby_properties = array( + 'id', + 'name', + 'created', + 'updated', + ); + + /** + * Check if the authenticated user can perform the request action. + * + * @since 1.0.0-beta.3 + * + * @return boolean + */ + protected function check_permissions() { + return current_user_can( 'manage_lifterlms_webhooks' ) ? true : llms_rest_authorization_required_error(); + } + + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Insert the prepared data into the database. + * + * @since 1.0.0-beta.3 + * + * @param array $prepared Prepared item data. + * @param WP_REST_Request $request Request object. + * @return obj Object Instance of object from $this->get_object(). + */ + protected function create_object( $prepared, $request ) { + + return LLMS_REST_API()->webhooks()->create( $prepared ); + + } + + /** + * Check if a given request has access to delete an item. + * + * @since 1.0.0-beta.3 + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Delete the object. + * + * Note: we do not return 404s when the resource to delete cannot be found. We assume it's already been deleted and respond with 204. + * Errors returned by this method should be any error other than a 404! + * + * @since 1.0.0-beta.3 + * + * @param obj $object Instance of the object from $this->get_object(). + * @param WP_REST_Request $request Request object. + * @return true|WP_Error true when the object is removed, WP_Error on failure. + */ + protected function delete_object( $object, $request ) { + + return $object->delete(); + + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.3 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + + $params = parent::get_collection_params(); + + $params['status'] = array( + 'description' => __( 'Include only webhooks matching a specific status.', 'lifterlms' ), + 'type' => 'string', + 'enum' => array_keys( LLMS_REST_API()->webhooks()->get_statuses() ), + ); + + return $params; + + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @since 1.0.0-beta.3 + * + * @return array + */ + public function get_item_schema() { + + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'api_key', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Webhook ID.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Friendly, human-readable name or description.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'status' => array( + 'description' => __( 'The status of the webhook.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array_keys( LLMS_REST_API()->webhooks()->get_statuses() ), + 'default' => 'disabled', + ), + 'topic' => array( + 'description' => __( 'The webhook topic', 'lifterlms' ), + 'type' => 'string', + 'required' => true, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => array( LLMS_REST_API()->webhooks(), 'is_topic_valid' ), + ), + ), + 'delivery_url' => array( + 'description' => __( 'The webhook payload delivery URL.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'secret' => array( + 'description' => __( 'An optional secret key used to generate the delivery signature.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'created' => array( + 'description' => __( 'Creation date. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated' => array( + 'description' => __( 'Date last modified. Format: Y-m-d H:i:s', 'lifterlms' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'resource' => array( + 'description' => __( 'The parsed resource from the `topic`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'The parsed event from the `topic`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'List of WordPress action hook associated with the webhook.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + } + + /** + * Check if a given request has access to read an item. + * + * @since 1.0.0-beta.3 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Check if a given request has access to read items. + * + * @since 1.0.0-beta.3 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + return $this->check_permissions(); + } + + /** + * Retrieve pagination information from an objects query. + * + * @since 1.0.0-beta.3 + * + * @param obj $query Objects query result. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + return array( + 'current_page' => $query->get( 'page' ), + 'total_results' => $query->found_results, + 'total_pages' => $query->max_pages, + ); + + } + + /** + * Retrieve An Webhook object by ID. + * + * @since 1.0.0-beta.3 + * + * @param int $id Webhook ID. + * @param bool $hydrate If true, pulls all key data from the database on instantiation. + * @return WP_Error|LLMS_REST_API_Key + */ + protected function get_object( $id, $hydrate = true ) { + + if ( ! is_numeric( $id ) ) { + $id = $this->get_object_id( $id ); + } + + $key = LLMS_REST_API()->webhooks()->get( $id, $hydrate ); + return $key ? $key : llms_rest_not_found_error(); + + } + + /** + * Retrieve a query object based on arguments from a `get_items()` (collection) request. + * + * @since 1.0.0-beta.3 + * + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return WP_User_Query + */ + protected function get_objects_query( $prepared, $request ) { + + return new LLMS_REST_Webhooks_Query( $prepared ); + + } + + /** + * Retrieve an array of objects from the result of $this->get_objects_query(). + * + * @since 1.0.0-beta.3 + * + * @param obj $query Objects query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + return $query->get_results(); + + } + + /** + * Map request keys to database keys for insertion. + * + * Array keys are the request fields (as defined in the schema) and + * array values are the database fields. + * + * @since 1.0.0-beta.3 + * + * @return array + */ + protected function map_schema_to_database() { + + $map = parent::map_schema_to_database(); + + // Not inserted/read via database calls. + unset( $map['resource'], $map['event'], $map['hooks'] ); + + return $map; + + } + + /** + * Prepare an object for response. + * + * @since 1.0.0-beta.3 + * + * @param LLMS_Abstract_User_Data $object User object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_object_for_response( $object, $request ) { + + $prepared = parent::prepare_object_for_response( $object, $request ); + + $prepared['id'] = absint( $prepared['id'] ); + $prepared['resource'] = $object->get_resource(); + $prepared['event'] = $object->get_event(); + $prepared['hooks'] = $object->get_hooks(); + $prepared['created'] = mysql_to_rfc3339( $prepared['created'] ); + $prepared['updated'] = mysql_to_rfc3339( $prepared['updated'] ); + + return $prepared; + + } + + /** + * Update an Webhook + * + * @since 1.0.0-beta.3 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + + $prepared = $this->prepare_item_for_database( $request ); + $key = LLMS_REST_API()->webhooks()->update( $prepared ); + if ( is_wp_error( $request ) ) { + $request->add_data( array( 'status' => 400 ) ); + return $request; + } + + $response = $this->prepare_item_for_response( $key, $request ); + + return $response; + + } + + /** + * Check if a given request has access to update an item. + * + * @since 1.0.0-beta.3 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + return $this->check_permissions(); + } + +} diff --git a/libraries/lifterlms-rest/includes/server/index.php b/libraries/lifterlms-rest/includes/server/index.php new file mode 100644 index 0000000000..22e2576755 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/index.php @@ -0,0 +1 @@ +<?php // quiet. diff --git a/libraries/lifterlms-rest/includes/server/llms-rest-server-functions.php b/libraries/lifterlms-rest/includes/server/llms-rest-server-functions.php new file mode 100644 index 0000000000..46bec3812b --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/llms-rest-server-functions.php @@ -0,0 +1,341 @@ +<?php +/** + * REST Server functions + * + * @package LifterLMS_REST/Functions + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.18 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * Return a WP_Error with proper code, message and status for unauthorized requests. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.12 Added a second paramater to avoid checking if the user is logged in. + * @since 1.0.0-beta.18 Use WP_Http constants for the error status. + * + * @param string $message Optional. The custom error message. Default empty string. + * When no custom message is provided a predefined message will be used. + * @param boolean $check_authenticated Optional. Whether or not checking if the current user is logged in. Default `true`. + * @return WP_Error + */ +function llms_rest_authorization_required_error( $message = '', $check_authenticated = true ) { + if ( $check_authenticated && is_user_logged_in() ) { + // 403. + $error_code = 'llms_rest_forbidden_request'; + $_message = __( 'You are not authorized to perform this request.', 'lifterlms' ); + $status = WP_Http::FORBIDDEN; // 403. + } else { + // 401. + $error_code = 'llms_rest_unauthorized_request'; + $_message = __( 'The API credentials were invalid.', 'lifterlms' ); + $status = WP_Http::UNAUTHORIZED; // 401. + } + + $message = ! $message ? $_message : $message; + return new WP_Error( $error_code, $message, array( 'status' => $status ) ); +} + +/** + * Return a WP_Error with proper code, message and status for invalid or malformed request syntax. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use WP_Http constant for the error status. + * + * @param string $message Optional. The custom error message. Default empty string. + * When no custom message is provided a predefined message will be used. + * @return WP_Error + */ +function llms_rest_bad_request_error( $message = '' ) { + $message = ! $message ? __( 'Invalid or malformed request syntax.', 'lifterlms' ) : $message; + return new WP_Error( 'llms_rest_bad_request', $message, array( 'status' => WP_Http::BAD_REQUEST ) ); // 400. +} + +/** + * Return a WP_Error with proper code, message and status for not found resources. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use WP_Http constant for the error status. + * + * @param string $message Optional. The custom error message. Default empty string. + * When no custom message is provided a predefined message will be used. + * @return WP_Error + */ +function llms_rest_not_found_error( $message = '' ) { + $message = ! $message ? __( 'The requested resource could not be found.', 'lifterlms' ) : $message; + return new WP_Error( 'llms_rest_not_found', $message, array( 'status' => WP_Http::NOT_FOUND ) ); // 404. +} + +/** + * Return a WP_Error for a 500 Internal Server Error. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.18 Use WP_Http constant for the error status. + * + * @param string $message Optional. Custom error message. When none provided a predefined message is used. + * @return WP_Error + */ +function llms_rest_server_error( $message = '' ) { + $message = ! $message ? __( 'Internal Server Error.', 'lifterlms' ) : $message; + return new WP_Error( 'llms_rest_server_error', $message, array( 'status' => WP_Http::INTERNAL_SERVER_ERROR ) ); // 500. +} + +/** + * Checks whether or not the passed object is a 401 (permission) or 403 (authorization) error + * + * @since 1.0.0-beta.18 + * + * @param WP_Error $wp_error The WP_Error object to check. + * @return boolean + */ +function llms_rest_is_authorization_required_error( $wp_error ) { + return ! empty( array_intersect( llms_rest_get_all_error_statuses( $wp_error ), array( WP_Http::FORBIDDEN, WP_Http::UNAUTHORIZED ) ) ); // 403, 401. +} + +/** + * Checks whether or not the passed object is a 400 bad request error + * + * @since 1.0.0-beta.18 + * + * @param WP_Error $wp_error The WP_Error object to check. + * @return boolean + */ +function llms_rest_is_bad_request_error( $wp_error ) { + return in_array( WP_Http::BAD_REQUEST, llms_rest_get_all_error_statuses( $wp_error ), true ); // 400. +} + +/** + * Checks whether or not the passed object is a 404 not found error + * + * @since 1.0.0-beta.18 + * + * @param WP_Error $wp_error The WP_Error object to check. + * @return boolean + */ +function llms_rest_is_not_found_error( $wp_error ) { + return in_array( WP_Http::NOT_FOUND, llms_rest_get_all_error_statuses( $wp_error ), true ); // 404. +} + +/** + * Checks whether or not the passed object is a 500 internal server error + * + * @since 1.0.0-beta.18 + * + * @param WP_Error $wp_error The WP_Error object to check. + * @return boolean + */ +function llms_rest_is_server_error( $wp_error ) { + return in_array( WP_Http::INTERNAL_SERVER_ERROR, llms_rest_get_all_error_statuses( $wp_error ), true ); // 500. +} + +/** + * Returns all the error statuses of a WP_Error + * + * @since 1.0.0-beta.18 + * + * @param WP_Error $wp_error The WP_Error object. + * @return int[] + */ +function llms_rest_get_all_error_statuses( $wp_error ) { + $statuses = array(); + + if ( is_wp_error( $wp_error ) && ! empty( $wp_error->has_errors() ) ) { + /** + * The method `get_all_error_data()` has been introduced in wp 5.6.0. + * TODO: remove bw compatibility when min wp version will be raised above 5.6.0. + */ + global $wp_version; + $func = ( version_compare( $wp_version, 5.6, '>=' ) ) ? 'get_all_error_data' : 'get_error_data'; + + foreach ( $wp_error->get_error_codes() as $code ) { + $status = $wp_error->{$func}( $code ); + $status = 'get_error_data' === $func ? array( $status ) : $status; + /** + * Use native `array_column()` in place of `wp_list_pluck()` as: + * 1) `$status` is fors ure an array (and not possibly an object); + * 2) `wp_list_pluck()` raises an error if the key ('status' in this case) is not found. + */ + $statuses = array_merge( $statuses, array_column( $status, 'status' ) ); + } + $statuses = array_filter( array_unique( $statuses ) ); + } + + return $statuses; + +} + +/** + * Validate submitted array of integers is an array of real user ids + * + * @since 1.0.0-beta.9 + * + * @param array $instructors Array of instructors id. + * @return boolean + */ +function llms_validate_instructors( $instructors ) { + return ! empty( $instructors ) ? count( array_filter( array_map( 'get_userdata', $instructors ) ) ) === count( $instructors ) : false; +} + +/** + * Validate strict positive integer number + * + * @since 1.0.0-beta.18 + * + * @param integer $number Integer number to validate. + * @return boolean + */ +function llms_rest_validate_strictly_positive_int( $number ) { + return llms_rest_validate_positive_int( $number, false ); +} + +/** + * Validate positive integer number including zero + * + * @since 1.0.0-beta.18 + * + * @param integer $number Integer number to validate. + * @return boolean + */ +function llms_rest_validate_positive_int_w_zero( $number ) { + return llms_rest_validate_positive_int( $number ); +} + + +/** + * Validate positive integer number + * + * @since 1.0.0-beta.18 + * + * @param integer $number Integer number to validate. + * @param boolean $include_zero Optional. Whether or not 0 is included. Default is `true`. + * @return boolean + */ +function llms_rest_validate_positive_int( $number, $include_zero = true ) { + return false !== filter_var( + $number, + FILTER_VALIDATE_INT, + array( + 'options' => array( + 'min_range' => $include_zero ? 0 : 1, + ), + ) + ); +} + +/** + * Validate strict positive float number + * + * @since 1.0.0-beta.18 + * + * @param integer $number Float number to validate. + * @return boolean + */ +function llms_rest_validate_strictly_positive_float( $number ) { + return llms_rest_validate_positive_float( $number, false ); +} + +/** + * Validate strict positive float number including zero + * + * @since 1.0.0-beta.18 + * + * @param integer $number Float number to validate. + * @return boolean + */ +function llms_rest_validate_positive_float_w_zero( $number ) { + return llms_rest_validate_positive_float( $number ); +} + +/** + * Validate strict positive float number + * + * @since [versoin] + * + * @param integer $number Float number to validate. + * @param boolean $include_zero Optional. Whether or not 0 is included. Default is `true`. + * @return boolean + */ +function llms_rest_validate_positive_float( $number, $include_zero = true ) { + // @TODO min_range and max_range options for FILTER_VALIDATE_FLOAT are only available since PHP 7.4. + $is_float = false !== filter_var( (float) $number, FILTER_VALIDATE_FLOAT ); + return $is_float && ( $include_zero ? $number >= 0 : $number > 0 ); +} + + +/** + * Validate submitted integer, or array of integers is an array of real memberships id, or empty. + * + * @since 1.0.0-beta.18 + * + * @param int|int[] $memberships Array of memberships id. + * @param boolean $allow_empty Optional. Whether or not allowing empty lists. Default false. + * @return boolean + */ +function llms_rest_validate_memberships( $memberships, $allow_empty = false ) { + return llms_rest_validate_post_types( $memberships, 'llms_membership', $allow_empty ); +} + + +/** + * Validate submitted array of integers is an array of real courses id, or empty. + * + * @since 1.0.0-beta.18 + * + * @param int|int[] $courses Array of courses id. + * @param boolean $allow_empty Optional. Whether or not allowing empty lists. Default false. + * @return boolean + */ +function llms_rest_validate_courses( $courses, $allow_empty = false ) { + return llms_rest_validate_post_types( $courses, 'course', $allow_empty ); +} + +/** + * Validate submitted array of integers is an array of real courses/memberships id, or empty. + * + * @since 1.0.0-beta.18 + * + * @param int|int[] $products Array of courses/memberships id. + * @param boolean $allow_empty Optional. Whether or not allowing empty lists. Default false. + * @return boolean + */ +function llms_rest_validate_products( $products, $allow_empty = false ) { + return llms_rest_validate_post_types( $products, array( 'course', 'llms_membership' ), $allow_empty ); +} + +/** + * Validate submitted array of integers is an array of real post types id, or empty. + * + * @param int|int[] $ids A single or a list of post IDs. + * @param string|string[] $post_types A single or a list of post types to check against. + * @param boolean $allow_empty Optional. Whether or not allowing empty lists. Default false. + * @return boolean + */ +function llms_rest_validate_post_types( $ids, $post_types, $allow_empty = false ) { + + $ids = is_array( $ids ) ? $ids : array( $ids ); + $ids = array_filter( $ids ); + + if ( empty( $ids ) ) { + return $allow_empty; + } + + $valid = true; + $post_types = is_array( $post_types ) ? $post_types : array( $post_types ); + + if ( ! empty( $ids ) ) { + $real_post_types = array_filter( + $ids, + function( $id ) use ( $post_types ) { + return ( is_numeric( $id ) && in_array( get_post_type( (int) $id ), $post_types, true ) ); + } + ); + + $valid = count( $real_post_types ) === count( $ids ); + } + + return $valid; + +} diff --git a/libraries/lifterlms-rest/includes/server/schemas/index.php b/libraries/lifterlms-rest/includes/server/schemas/index.php new file mode 100644 index 0000000000..bba9ee3cf5 --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/schemas/index.php @@ -0,0 +1 @@ +<?php // Quiet. diff --git a/libraries/lifterlms-rest/includes/server/schemas/schema-access-plans.php b/libraries/lifterlms-rest/includes/server/schemas/schema-access-plans.php new file mode 100644 index 0000000000..20ec60841b --- /dev/null +++ b/libraries/lifterlms-rest/includes/server/schemas/schema-access-plans.php @@ -0,0 +1,234 @@ +<?php +/** + * Access plan schema definition + * + * This schema contains all properties of the access plan that are not inherited from the parent schema. + * + * @package LifterLMS/Classes + * + * @since 1.0.0-beta.18 + * @version 1.0.0-beta.18 + * + * @see LLMS_REST_Access_Plans_Controller::get_item_schema() + */ + +defined( 'ABSPATH' ) || exit; + +return array( + 'price' => array( + 'description' => __( 'Access plan price.', 'lifterlms' ), + 'type' => 'number', + 'required' => true, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_positive_float_w_zero', + ), + ), + 'access_expiration' => array( + 'description' => __( 'Access expiration type. `lifetime` provides access until cancelled or until a recurring payment fails. `limited-period` provides access for a limited period as specified by `access_length` and `access_period` `limited-date` provides access until the date specified by access_expires_date`.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'lifetime', + 'enum' => array( + 'lifetime', + 'limited-period', + 'limited-date', + ), + 'context' => array( 'view', 'edit' ), + ), + 'access_expires' => array( + 'description' => __( 'Date when access expires. Only applicable when `access_expiration` is `limited-date`. `Format: Y-m-d H:i:s`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'access_length' => array( + 'description' => __( 'Determine the length of access from time of purchase. Only applicable when `access_expiration` is `limited-period`.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'default' => 1, + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_strictly_positive_int', + 'sanitize_callback' => 'absint', + ), + ), + 'access_period' => array( + 'description' => __( 'Determine the length of access from time of purchase. Only applicable when `access_expiration` is `limited-period`', 'lifterlms' ), + 'type' => 'string', + 'default' => 'year', + 'enum' => array_keys( llms_get_access_plan_period_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'availability_restrictions' => array( + 'description' => __( 'Restrict usage of this access plan to students enrolled in at least one of the specified memberships.', 'lifterlms' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => static function ( $val ) { + return llms_rest_validate_memberships( $val, true ); // Allow empty to unset. + }, + ), + + ), + 'enroll_text' => array( + 'description' => __( 'Text of the "Purchase" button', 'lifterlms' ), + 'type' => 'string', + 'default' => __( 'Buy Now', 'lifterlms' ), + 'context' => array( 'view', 'edit' ), + ), + 'frequency' => array( + 'description' => __( 'Billing frequency [0-6]. `0` denotes a one-time payment. `>= 1` denotes a recurring plan.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => static function ( $val ) { + return in_array( $val, range( 0, 6 ), true ) ? true : new WP_Error( + 'rest_invalid_param', + __( 'Must be an integer in the range 0-6', 'lifterlms' ) + ); + }, + 'sanitize_callback' => 'absint', + ), + ), + 'length' => array( + 'description' => __( 'For recurring plans only. Determines the number of intervals a plan should run for. `0` denotes the plan should run until cancelled.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'period' => array( + 'description' => __( 'For recurring plans only. Determines the interval of recurring payments.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'year', + 'enum' => array_keys( llms_get_access_plan_period_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'post_id' => array( + 'description' => __( 'Determines the course or membership which can be accessed through the plan.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'required' => true, + 'arg_options' => array( + 'validate_callback' => static function ( $val ) { + return llms_rest_validate_products( $val ) ? true : new WP_Error( + 'rest_invalid_param', + __( 'Must be a valid course or membership ID', 'lifterlms' ) + ); + }, + 'sanitize_callback' => 'absint', + ), + ), + 'redirect_forced' => array( + 'description' => __( "Use this plans's redirect settings when purchasing a Membership this plan is restricted to. Applicable only when `availability_restrictions` exist for the plan", 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'redirect_page' => array( + 'description' => __( 'WordPress page ID to use for checkout success redirection. Applicable only when `redirect_type` is page.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_strictly_positive_int', + 'sanitize_callback' => 'absint', + ), + ), + 'redirect_type' => array( + 'description' => __( "Determines the redirection behavior of the user's browser upon successful checkout or registration through the plan. `self`: Redirect to the permalink of the specified `post_id`. `page`: Redirect to the permalink of the WordPress page specified by `redirect_page_id`. `url`: Redirect to the URL specified by `redirect_url`.", 'lifterlms' ), + 'type' => 'string', + 'default' => 'self', + 'enum' => array( + 'self', + 'page', + 'url', + ), + 'context' => array( 'view', 'edit' ), + ), + 'redirect_url' => array( + 'description' => __( 'URL to use for checkout success redirection. Applicable only when `redirect_type` is `url`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'format' => 'uri', + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), + ), + 'sale_date_end' => array( + 'description' => __( 'Used to automatically end a scheduled sale. If empty, the plan remains on sale indefinitely. Only applies when `sale_enabled` is `true`. Format: `Y-m-d H:i:s`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_date_start' => array( + 'description' => __( 'Used to automatically start a scheduled sale. If empty, the plan is on sale immediately. Only applies when `sale_enabled` is `true`. Format: `Y-m-d H:i:s`.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_enabled' => array( + 'description' => __( 'Mark the plan as "On Sale" allowing for temporary price adjustments.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Sale price. Only applies when `sale_enabled` is `true`.', 'lifterlms' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_positive_float_w_zero', + ), + ), + 'sku' => array( + 'description' => __( 'External identifier', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'trial_enabled' => array( + 'description' => __( 'Enable a trial period for a recurring access plan.', 'lifterlms' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'trial_length' => array( + 'description' => __( 'Determines the length of trial access. Only applies when `trial_enabled` is `true`.', 'lifterlms' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_strictly_positive_int', + 'sanitize_callback' => 'absint', + ), + ), + 'trial_period' => array( + 'description' => __( 'Determines the length of trial access. Only applies when `trial_enabled` is `true`.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'week', + 'enum' => array( + 'year', + 'month', + 'week', + 'day', + ), + 'context' => array( 'view', 'edit' ), + ), + 'trial_price' => array( + 'description' => __( 'Determines the price of the trial period. Only applies when `trial_enabled` is `true`.', 'lifterlms' ), + 'type' => 'number', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'validate_callback' => 'llms_rest_validate_positive_float_w_zero', + ), + ), + 'visibility' => array( + 'description' => __( 'Access plan visibility.', 'lifterlms' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array_keys( llms_get_access_plan_visibility_options() ), + 'context' => array( 'view', 'edit' ), + ), +); diff --git a/libraries/lifterlms-rest/includes/traits/class-llms-rest-trait-singleton.php b/libraries/lifterlms-rest/includes/traits/class-llms-rest-trait-singleton.php new file mode 100644 index 0000000000..9a2089a37c --- /dev/null +++ b/libraries/lifterlms-rest/includes/traits/class-llms-rest-trait-singleton.php @@ -0,0 +1,53 @@ +<?php +/** + * Singleton class trait. + * + * @package LifterLMS_REST/Classes + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * LLMS_REST_Trait_Singleton class.. + * + * @since 1.0.0-beta.1 + */ +trait LLMS_REST_Trait_Singleton { + + /** + * Singleton instance of the class. + * + * @var obj + */ + private static $instance = null; + + /** + * Private Constructor. + * + * @since 1.0.0-beta.1 + * + * @return void + */ + private function __construct() {} + + /** + * Singleton Instance of the LifterLMS_REST_API class. + * + * @since 1.0.0-beta.1 + * + * @return obj instance of the LifterLMS_REST_API class. + */ + public static function instance() { + + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + + return self::$instance; + + } + +} diff --git a/libraries/lifterlms-rest/includes/traits/index.php b/libraries/lifterlms-rest/includes/traits/index.php new file mode 100644 index 0000000000..22e2576755 --- /dev/null +++ b/libraries/lifterlms-rest/includes/traits/index.php @@ -0,0 +1 @@ +<?php // quiet. diff --git a/libraries/lifterlms-rest/index.php b/libraries/lifterlms-rest/index.php new file mode 100644 index 0000000000..9c65c1efa6 --- /dev/null +++ b/libraries/lifterlms-rest/index.php @@ -0,0 +1 @@ +<?php // shhh. diff --git a/libraries/lifterlms-rest/lifterlms-rest.php b/libraries/lifterlms-rest/lifterlms-rest.php new file mode 100644 index 0000000000..099b44bf40 --- /dev/null +++ b/libraries/lifterlms-rest/lifterlms-rest.php @@ -0,0 +1,71 @@ +<?php +/** + * LifterLMS REST API Plugin + * + * @package LifterLMS_REST_API/Main + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + * + * Plugin Name: LifterLMS REST API + * Plugin URI: https://lifterlms.com/ + * Description: REST API feature plugin for the LifterLMS Core. + * Version: 1.0.0-beta.21 + * Author: LifterLMS + * Author URI: https://lifterlms.com/ + * Text Domain: lifterlms + * Domain Path: /i18n + * License: GPLv3 + * License URI: https://www.gnu.org/licenses/gpl-3.0.html + * Requires LifterLMS: 3.32.0 + */ + +defined( 'ABSPATH' ) || exit; + +// Don't load the REST API. +if ( defined( 'LLMS_REST_DISABLE' ) && LLMS_REST_DISABLE ) { + return; +} + +// @todo handle this better. +if ( version_compare( phpversion(), '7.1', '<' ) ) { + return; +} + +// Define Constants. +if ( ! defined( 'LLMS_REST_API_PLUGIN_FILE' ) ) { + define( 'LLMS_REST_API_PLUGIN_FILE', __FILE__ ); +} + +if ( ! defined( 'LLMS_REST_API_PLUGIN_DIR' ) ) { + define( 'LLMS_REST_API_PLUGIN_DIR', dirname( __FILE__ ) . '/' ); +} + +if ( ! defined( 'LLMS_REST_API_PLUGIN_URL' ) ) { + define( 'LLMS_REST_API_PLUGIN_URL', trailingslashit( plugin_dir_url( __FILE__ ) ) ); +} + +if ( ! defined( 'LLMS_REST_WEBHOOK_DELIVERY_LOGGING' ) ) { + define( 'LLMS_REST_WEBHOOK_DELIVERY_LOGGING', true ); +} + +// Load Plugin. +if ( ! class_exists( 'LifterLMS_REST_API' ) ) { + + require_once LLMS_REST_API_PLUGIN_DIR . 'class-lifterlms-rest-api.php'; + + // phpcs:disable WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid + /** + * Main Plugin Instance + * + * @since 1.0.0-beta.1 + * + * @return LLMS_REST_API + */ + function LLMS_REST_API() { + return LifterLMS_REST_API::instance(); + } +} + +return LLMS_REST_API(); +// phpcs:enable diff --git a/libraries/lifterlms-rest/uninstall.php b/libraries/lifterlms-rest/uninstall.php new file mode 100644 index 0000000000..78491dac8a --- /dev/null +++ b/libraries/lifterlms-rest/uninstall.php @@ -0,0 +1,31 @@ +<?php +/** + * LifterLMS REST API Uninstall + * + * @package LifterLMS_REST/Uninstall + * + * @since 1.0.0-beta.1 + * @version 1.0.0-beta.1 + */ + +// If uninstall not called from WordPress exit. +if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { + exit(); +} + +/** + * Only actually delete LifterLMS and Related Data when constant is defined + * This will prevent data loss when a plugin is deactivated + */ +if ( ! defined( 'LLMS_REMOVE_ALL_DATA' ) || true !== LLMS_REMOVE_ALL_DATA ) { + exit(); +} + +global $wpdb; + +// Delete options. +$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'lifterlms\_rest\_%';" ); +$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'llms\_rest\_%';" ); + +// drop tables. +$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}lifterms_api_keys" ); diff --git a/lifterlms.php b/lifterlms.php index 2245bc1e5b..db1f74db3f 100644 --- a/lifterlms.php +++ b/lifterlms.php @@ -10,7 +10,7 @@ * Plugin Name: LifterLMS * Plugin URI: https://lifterlms.com/ * Description: 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. - * Version: 5.9.0 + * Version: 5.10.0 * Author: LifterLMS * Author URI: https://lifterlms.com/ * Text Domain: lifterlms diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index cd0b86aaf4..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,35025 +0,0 @@ -{ - "name": "lifterlms", - "version": "5.9.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@ampproject/remapping": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.1.tgz", - "integrity": "sha512-Aolwjd7HSC2PyY0fDj/wA/EimQT4HfEnFYNp5s9CQlrdhyvWTtvZ5YzrUPu6R6/1jKiUlxu8bUhkdSnKHNAHMA==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/code-frame": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", - "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/compat-data": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", - "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", - "dev": true - }, - "@babel/core": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", - "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.15.8", - "@babel/generator": "^7.15.8", - "@babel/helper-compilation-targets": "^7.15.4", - "@babel/helper-module-transforms": "^7.15.8", - "@babel/helpers": "^7.15.4", - "@babel/parser": "^7.15.8", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.6", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/eslint-parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz", - "integrity": "sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==", - "dev": true, - "requires": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", - "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", - "dev": true, - "requires": { - "@babel/types": "^7.15.6", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", - "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", - "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.15.0", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - }, - "dependencies": { - "browserslist": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", - "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001271", - "electron-to-chromium": "^1.3.878", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz", - "integrity": "sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz", - "integrity": "sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", - "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^5.0.1" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", - "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-function-name": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", - "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", - "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", - "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", - "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-module-imports": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", - "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", - "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.15.4", - "@babel/helper-replace-supers": "^7.15.4", - "@babel/helper-simple-access": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/helper-validator-identifier": "^7.15.7", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.6" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", - "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", - "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-wrap-function": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-replace-supers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", - "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.15.4", - "@babel/helper-optimise-call-expression": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", - "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", - "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.0" - }, - "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", - "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", - "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/helpers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", - "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", - "dev": true, - "requires": { - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } - } - }, - "@babel/parser": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", - "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", - "dev": true - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", - "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", - "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.7" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", - "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", - "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz", - "integrity": "sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", - "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", - "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", - "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", - "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", - "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", - "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz", - "integrity": "sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.16.7" - }, - "dependencies": { - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", - "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", - "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.16.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", - "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.10", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", - "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", - "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", - "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", - "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", - "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", - "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-remap-async-to-generator": "^7.16.8" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", - "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", - "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", - "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz", - "integrity": "sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", - "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz", - "integrity": "sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", - "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", - "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", - "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", - "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", - "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/plugin-transform-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", - "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", - "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", - "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz", - "integrity": "sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", - "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", - "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz", - "integrity": "sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", - "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", - "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz", - "integrity": "sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", - "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", - "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-constant-elements": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.16.7.tgz", - "integrity": "sha512-lF+cfsyTgwWkcw715J88JhMYJ5GpysYNLhLP1PkvkhTRN7B3e74R/1KsDxFxhRpSn0UUD3IWM4GvdBR2PEbbQQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz", - "integrity": "sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz", - "integrity": "sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/plugin-transform-react-jsx-development": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", - "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", - "dev": true, - "requires": { - "@babel/plugin-transform-react-jsx": "^7.16.7" - } - }, - "@babel/plugin-transform-react-pure-annotations": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz", - "integrity": "sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz", - "integrity": "sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", - "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz", - "integrity": "sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "semver": "^6.3.0" - }, - "dependencies": { - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", - "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", - "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", - "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", - "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", - "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", - "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/plugin-syntax-typescript": "^7.16.7" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", - "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", - "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/preset-env": { - "version": "7.16.11", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz", - "integrity": "sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.8", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-async-generator-functions": "^7.16.8", - "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-proposal-class-static-block": "^7.16.7", - "@babel/plugin-proposal-dynamic-import": "^7.16.7", - "@babel/plugin-proposal-export-namespace-from": "^7.16.7", - "@babel/plugin-proposal-json-strings": "^7.16.7", - "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", - "@babel/plugin-proposal-numeric-separator": "^7.16.7", - "@babel/plugin-proposal-object-rest-spread": "^7.16.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", - "@babel/plugin-proposal-optional-chaining": "^7.16.7", - "@babel/plugin-proposal-private-methods": "^7.16.11", - "@babel/plugin-proposal-private-property-in-object": "^7.16.7", - "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.16.7", - "@babel/plugin-transform-async-to-generator": "^7.16.8", - "@babel/plugin-transform-block-scoped-functions": "^7.16.7", - "@babel/plugin-transform-block-scoping": "^7.16.7", - "@babel/plugin-transform-classes": "^7.16.7", - "@babel/plugin-transform-computed-properties": "^7.16.7", - "@babel/plugin-transform-destructuring": "^7.16.7", - "@babel/plugin-transform-dotall-regex": "^7.16.7", - "@babel/plugin-transform-duplicate-keys": "^7.16.7", - "@babel/plugin-transform-exponentiation-operator": "^7.16.7", - "@babel/plugin-transform-for-of": "^7.16.7", - "@babel/plugin-transform-function-name": "^7.16.7", - "@babel/plugin-transform-literals": "^7.16.7", - "@babel/plugin-transform-member-expression-literals": "^7.16.7", - "@babel/plugin-transform-modules-amd": "^7.16.7", - "@babel/plugin-transform-modules-commonjs": "^7.16.8", - "@babel/plugin-transform-modules-systemjs": "^7.16.7", - "@babel/plugin-transform-modules-umd": "^7.16.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8", - "@babel/plugin-transform-new-target": "^7.16.7", - "@babel/plugin-transform-object-super": "^7.16.7", - "@babel/plugin-transform-parameters": "^7.16.7", - "@babel/plugin-transform-property-literals": "^7.16.7", - "@babel/plugin-transform-regenerator": "^7.16.7", - "@babel/plugin-transform-reserved-words": "^7.16.7", - "@babel/plugin-transform-shorthand-properties": "^7.16.7", - "@babel/plugin-transform-spread": "^7.16.7", - "@babel/plugin-transform-sticky-regex": "^7.16.7", - "@babel/plugin-transform-template-literals": "^7.16.7", - "@babel/plugin-transform-typeof-symbol": "^7.16.7", - "@babel/plugin-transform-unicode-escapes": "^7.16.7", - "@babel/plugin-transform-unicode-regex": "^7.16.7", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.16.8", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "core-js-compat": "^3.20.2", - "semver": "^6.3.0" - }, - "dependencies": { - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-react": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz", - "integrity": "sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-react-display-name": "^7.16.7", - "@babel/plugin-transform-react-jsx": "^7.16.7", - "@babel/plugin-transform-react-jsx-development": "^7.16.7", - "@babel/plugin-transform-react-pure-annotations": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - } - } - }, - "@babel/preset-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", - "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-validator-option": "^7.16.7", - "@babel/plugin-transform-typescript": "^7.16.7" - }, - "dependencies": { - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - } - } - }, - "@babel/runtime": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", - "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/runtime-corejs3": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.17.2.tgz", - "integrity": "sha512-NcKtr2epxfIrNM4VOmPKO46TvDMCBhgi2CrSHaEarrz+Plk2K5r9QemmOFTGpZaoKnWoGH5MO+CzeRsih/Fcgg==", - "dev": true, - "requires": { - "core-js-pure": "^3.20.2", - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", - "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, - "@babel/traverse": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", - "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", - "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.9", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", - "dev": true - }, - "@es-joy/jsdoccomment": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.19.0.tgz", - "integrity": "sha512-lRx/5ChsOwv7gIU05m8Ur1Rxa4/XkE23wTsX8XFBGWRYrCcCrngPf6yGJMG6n9dqnyDehPrBBVeFIm2INEIeQA==", - "dev": true, - "requires": { - "comment-parser": "1.3.0", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~2.2.2" - }, - "dependencies": { - "comment-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.0.tgz", - "integrity": "sha512-hRpmWIKgzd81vn0ydoWoyPoALEOnF4wt8yKD35Ib1D6XC2siLiYaiqfGkYrunuKdsXGwpBpHU3+9r+RVw2NZfA==", - "dev": true - } - } - }, - "@eslint/eslintrc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz", - "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } - } - }, - "@gar/promisify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", - "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", - "dev": true - }, - "@gulp-sourcemaps/identity-map": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-ciiioYMLdo16ShmfHBXJBOFm3xPC4AuwO4xeRpFeHz7WK9PYsWCmigagG2XyzZpubK4a3qNKoUBDhbzHfa50LQ==", - "dev": true, - "requires": { - "acorn": "^5.0.3", - "css": "^2.2.1", - "normalize-path": "^2.1.1", - "source-map": "^0.6.0", - "through2": "^2.0.3" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@gulp-sourcemaps/map-sources": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", - "dev": true, - "requires": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - } - }, - "@hapi/hoek": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", - "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==", - "dev": true - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@humanwhocodes/config-array": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", - "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@hutson/parse-repository-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", - "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - } - }, - "@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - } - }, - "@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@lerna/add": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz", - "integrity": "sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng==", - "dev": true, - "requires": { - "@lerna/bootstrap": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/npm-conf": "4.0.0", - "@lerna/validation-error": "4.0.0", - "dedent": "^0.7.0", - "npm-package-arg": "^8.1.0", - "p-map": "^4.0.0", - "pacote": "^11.2.6", - "semver": "^7.3.4" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "@lerna/bootstrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz", - "integrity": "sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/has-npm-version": "4.0.0", - "@lerna/npm-install": "4.0.0", - "@lerna/package-graph": "4.0.0", - "@lerna/pulse-till-done": "4.0.0", - "@lerna/rimraf-dir": "4.0.0", - "@lerna/run-lifecycle": "4.0.0", - "@lerna/run-topologically": "4.0.0", - "@lerna/symlink-binary": "4.0.0", - "@lerna/symlink-dependencies": "4.0.0", - "@lerna/validation-error": "4.0.0", - "dedent": "^0.7.0", - "get-port": "^5.1.1", - "multimatch": "^5.0.0", - "npm-package-arg": "^8.1.0", - "npmlog": "^4.1.2", - "p-map": "^4.0.0", - "p-map-series": "^2.1.0", - "p-waterfall": "^2.1.1", - "read-package-tree": "^5.3.1", - "semver": "^7.3.4" - }, - "dependencies": { - "array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, - "multimatch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", - "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", - "dev": true, - "requires": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "@lerna/changed": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz", - "integrity": "sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ==", - "dev": true, - "requires": { - "@lerna/collect-updates": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/listable": "4.0.0", - "@lerna/output": "4.0.0" - } - }, - "@lerna/check-working-tree": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz", - "integrity": "sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q==", - "dev": true, - "requires": { - "@lerna/collect-uncommitted": "4.0.0", - "@lerna/describe-ref": "4.0.0", - "@lerna/validation-error": "4.0.0" - } - }, - "@lerna/child-process": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz", - "integrity": "sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "execa": "^5.0.0", - "strong-log-transformer": "^2.1.0" - } - }, - "@lerna/clean": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz", - "integrity": "sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/prompt": "4.0.0", - "@lerna/pulse-till-done": "4.0.0", - "@lerna/rimraf-dir": "4.0.0", - "p-map": "^4.0.0", - "p-map-series": "^2.1.0", - "p-waterfall": "^2.1.1" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "@lerna/cli": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz", - "integrity": "sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA==", - "dev": true, - "requires": { - "@lerna/global-options": "4.0.0", - "dedent": "^0.7.0", - "npmlog": "^4.1.2", - "yargs": "^16.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "@lerna/collect-uncommitted": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz", - "integrity": "sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "chalk": "^4.1.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/collect-updates": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz", - "integrity": "sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/describe-ref": "4.0.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "@lerna/command": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz", - "integrity": "sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/package-graph": "4.0.0", - "@lerna/project": "4.0.0", - "@lerna/validation-error": "4.0.0", - "@lerna/write-log-file": "4.0.0", - "clone-deep": "^4.0.1", - "dedent": "^0.7.0", - "execa": "^5.0.0", - "is-ci": "^2.0.0", - "npmlog": "^4.1.2" - }, - "dependencies": { - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - } - } - }, - "@lerna/conventional-commits": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz", - "integrity": "sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw==", - "dev": true, - "requires": { - "@lerna/validation-error": "4.0.0", - "conventional-changelog-angular": "^5.0.12", - "conventional-changelog-core": "^4.2.2", - "conventional-recommended-bump": "^6.1.0", - "fs-extra": "^9.1.0", - "get-stream": "^6.0.0", - "lodash.template": "^4.5.0", - "npm-package-arg": "^8.1.0", - "npmlog": "^4.1.2", - "pify": "^5.0.0", - "semver": "^7.3.4" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "dev": true - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/create": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz", - "integrity": "sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/npm-conf": "4.0.0", - "@lerna/validation-error": "4.0.0", - "dedent": "^0.7.0", - "fs-extra": "^9.1.0", - "globby": "^11.0.2", - "init-package-json": "^2.0.2", - "npm-package-arg": "^8.1.0", - "p-reduce": "^2.1.0", - "pacote": "^11.2.6", - "pify": "^5.0.0", - "semver": "^7.3.4", - "slash": "^3.0.0", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^3.0.0", - "whatwg-url": "^8.4.0", - "yargs-parser": "20.2.4" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - } - } - }, - "@lerna/create-symlink": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz", - "integrity": "sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig==", - "dev": true, - "requires": { - "cmd-shim": "^4.1.0", - "fs-extra": "^9.1.0", - "npmlog": "^4.1.2" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/describe-ref": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz", - "integrity": "sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz", - "integrity": "sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/validation-error": "4.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/exec": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz", - "integrity": "sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/profiler": "4.0.0", - "@lerna/run-topologically": "4.0.0", - "@lerna/validation-error": "4.0.0", - "p-map": "^4.0.0" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "@lerna/filter-options": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz", - "integrity": "sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw==", - "dev": true, - "requires": { - "@lerna/collect-updates": "4.0.0", - "@lerna/filter-packages": "4.0.0", - "dedent": "^0.7.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/filter-packages": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz", - "integrity": "sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA==", - "dev": true, - "requires": { - "@lerna/validation-error": "4.0.0", - "multimatch": "^5.0.0", - "npmlog": "^4.1.2" - }, - "dependencies": { - "array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, - "multimatch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", - "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", - "dev": true, - "requires": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - } - } - } - }, - "@lerna/get-npm-exec-opts": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz", - "integrity": "sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/get-packed": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz", - "integrity": "sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w==", - "dev": true, - "requires": { - "fs-extra": "^9.1.0", - "ssri": "^8.0.1", - "tar": "^6.1.0" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@lerna/github-client": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz", - "integrity": "sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@octokit/plugin-enterprise-rest": "^6.0.1", - "@octokit/rest": "^18.1.0", - "git-url-parse": "^11.4.4", - "npmlog": "^4.1.2" - } - }, - "@lerna/gitlab-client": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz", - "integrity": "sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA==", - "dev": true, - "requires": { - "node-fetch": "^2.6.1", - "npmlog": "^4.1.2", - "whatwg-url": "^8.4.0" - } - }, - "@lerna/global-options": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz", - "integrity": "sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ==", - "dev": true - }, - "@lerna/has-npm-version": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz", - "integrity": "sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "semver": "^7.3.4" - } - }, - "@lerna/import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz", - "integrity": "sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/prompt": "4.0.0", - "@lerna/pulse-till-done": "4.0.0", - "@lerna/validation-error": "4.0.0", - "dedent": "^0.7.0", - "fs-extra": "^9.1.0", - "p-map-series": "^2.1.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/info": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz", - "integrity": "sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/output": "4.0.0", - "envinfo": "^7.7.4" - } - }, - "@lerna/init": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz", - "integrity": "sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/command": "4.0.0", - "fs-extra": "^9.1.0", - "p-map": "^4.0.0", - "write-json-file": "^4.3.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/link": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz", - "integrity": "sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/package-graph": "4.0.0", - "@lerna/symlink-dependencies": "4.0.0", - "p-map": "^4.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "@lerna/list": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz", - "integrity": "sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/listable": "4.0.0", - "@lerna/output": "4.0.0" - } - }, - "@lerna/listable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz", - "integrity": "sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ==", - "dev": true, - "requires": { - "@lerna/query-graph": "4.0.0", - "chalk": "^4.1.0", - "columnify": "^1.5.4" - } - }, - "@lerna/log-packed": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz", - "integrity": "sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ==", - "dev": true, - "requires": { - "byte-size": "^7.0.0", - "columnify": "^1.5.4", - "has-unicode": "^2.0.1", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-conf": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz", - "integrity": "sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw==", - "dev": true, - "requires": { - "config-chain": "^1.1.12", - "pify": "^5.0.0" - }, - "dependencies": { - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "dev": true - } - } - }, - "@lerna/npm-dist-tag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz", - "integrity": "sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw==", - "dev": true, - "requires": { - "@lerna/otplease": "4.0.0", - "npm-package-arg": "^8.1.0", - "npm-registry-fetch": "^9.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/npm-install": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz", - "integrity": "sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/get-npm-exec-opts": "4.0.0", - "fs-extra": "^9.1.0", - "npm-package-arg": "^8.1.0", - "npmlog": "^4.1.2", - "signal-exit": "^3.0.3", - "write-pkg": "^4.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/npm-publish": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz", - "integrity": "sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w==", - "dev": true, - "requires": { - "@lerna/otplease": "4.0.0", - "@lerna/run-lifecycle": "4.0.0", - "fs-extra": "^9.1.0", - "libnpmpublish": "^4.0.0", - "npm-package-arg": "^8.1.0", - "npmlog": "^4.1.2", - "pify": "^5.0.0", - "read-package-json": "^3.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "dev": true - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/npm-run-script": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz", - "integrity": "sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "@lerna/get-npm-exec-opts": "4.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/otplease": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz", - "integrity": "sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw==", - "dev": true, - "requires": { - "@lerna/prompt": "4.0.0" - } - }, - "@lerna/output": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz", - "integrity": "sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/pack-directory": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz", - "integrity": "sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ==", - "dev": true, - "requires": { - "@lerna/get-packed": "4.0.0", - "@lerna/package": "4.0.0", - "@lerna/run-lifecycle": "4.0.0", - "npm-packlist": "^2.1.4", - "npmlog": "^4.1.2", - "tar": "^6.1.0", - "temp-write": "^4.0.0" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@lerna/package": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz", - "integrity": "sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q==", - "dev": true, - "requires": { - "load-json-file": "^6.2.0", - "npm-package-arg": "^8.1.0", - "write-pkg": "^4.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", - "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^5.0.0", - "strip-bom": "^4.0.0", - "type-fest": "^0.6.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "@lerna/package-graph": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz", - "integrity": "sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw==", - "dev": true, - "requires": { - "@lerna/prerelease-id-from-version": "4.0.0", - "@lerna/validation-error": "4.0.0", - "npm-package-arg": "^8.1.0", - "npmlog": "^4.1.2", - "semver": "^7.3.4" - } - }, - "@lerna/prerelease-id-from-version": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz", - "integrity": "sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg==", - "dev": true, - "requires": { - "semver": "^7.3.4" - } - }, - "@lerna/profiler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz", - "integrity": "sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q==", - "dev": true, - "requires": { - "fs-extra": "^9.1.0", - "npmlog": "^4.1.2", - "upath": "^2.0.1" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "upath": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", - "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", - "dev": true - } - } - }, - "@lerna/project": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz", - "integrity": "sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg==", - "dev": true, - "requires": { - "@lerna/package": "4.0.0", - "@lerna/validation-error": "4.0.0", - "cosmiconfig": "^7.0.0", - "dedent": "^0.7.0", - "dot-prop": "^6.0.1", - "glob-parent": "^5.1.1", - "globby": "^11.0.2", - "load-json-file": "^6.2.0", - "npmlog": "^4.1.2", - "p-map": "^4.0.0", - "resolve-from": "^5.0.0", - "write-json-file": "^4.3.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "load-json-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", - "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^5.0.0", - "strip-bom": "^4.0.0", - "type-fest": "^0.6.0" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "@lerna/prompt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz", - "integrity": "sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ==", - "dev": true, - "requires": { - "inquirer": "^7.3.3", - "npmlog": "^4.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "@lerna/publish": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz", - "integrity": "sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg==", - "dev": true, - "requires": { - "@lerna/check-working-tree": "4.0.0", - "@lerna/child-process": "4.0.0", - "@lerna/collect-updates": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/describe-ref": "4.0.0", - "@lerna/log-packed": "4.0.0", - "@lerna/npm-conf": "4.0.0", - "@lerna/npm-dist-tag": "4.0.0", - "@lerna/npm-publish": "4.0.0", - "@lerna/otplease": "4.0.0", - "@lerna/output": "4.0.0", - "@lerna/pack-directory": "4.0.0", - "@lerna/prerelease-id-from-version": "4.0.0", - "@lerna/prompt": "4.0.0", - "@lerna/pulse-till-done": "4.0.0", - "@lerna/run-lifecycle": "4.0.0", - "@lerna/run-topologically": "4.0.0", - "@lerna/validation-error": "4.0.0", - "@lerna/version": "4.0.0", - "fs-extra": "^9.1.0", - "libnpmaccess": "^4.0.1", - "npm-package-arg": "^8.1.0", - "npm-registry-fetch": "^9.0.0", - "npmlog": "^4.1.2", - "p-map": "^4.0.0", - "p-pipe": "^3.1.0", - "pacote": "^11.2.6", - "semver": "^7.3.4" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/pulse-till-done": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz", - "integrity": "sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/query-graph": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz", - "integrity": "sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg==", - "dev": true, - "requires": { - "@lerna/package-graph": "4.0.0" - } - }, - "@lerna/resolve-symlink": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz", - "integrity": "sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA==", - "dev": true, - "requires": { - "fs-extra": "^9.1.0", - "npmlog": "^4.1.2", - "read-cmd-shim": "^2.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/rimraf-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz", - "integrity": "sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg==", - "dev": true, - "requires": { - "@lerna/child-process": "4.0.0", - "npmlog": "^4.1.2", - "path-exists": "^4.0.0", - "rimraf": "^3.0.2" - }, - "dependencies": { - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "@lerna/run": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz", - "integrity": "sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ==", - "dev": true, - "requires": { - "@lerna/command": "4.0.0", - "@lerna/filter-options": "4.0.0", - "@lerna/npm-run-script": "4.0.0", - "@lerna/output": "4.0.0", - "@lerna/profiler": "4.0.0", - "@lerna/run-topologically": "4.0.0", - "@lerna/timer": "4.0.0", - "@lerna/validation-error": "4.0.0", - "p-map": "^4.0.0" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "@lerna/run-lifecycle": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz", - "integrity": "sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ==", - "dev": true, - "requires": { - "@lerna/npm-conf": "4.0.0", - "npm-lifecycle": "^3.1.5", - "npmlog": "^4.1.2" - } - }, - "@lerna/run-topologically": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz", - "integrity": "sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA==", - "dev": true, - "requires": { - "@lerna/query-graph": "4.0.0", - "p-queue": "^6.6.2" - } - }, - "@lerna/symlink-binary": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz", - "integrity": "sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA==", - "dev": true, - "requires": { - "@lerna/create-symlink": "4.0.0", - "@lerna/package": "4.0.0", - "fs-extra": "^9.1.0", - "p-map": "^4.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/symlink-dependencies": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz", - "integrity": "sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw==", - "dev": true, - "requires": { - "@lerna/create-symlink": "4.0.0", - "@lerna/resolve-symlink": "4.0.0", - "@lerna/symlink-binary": "4.0.0", - "fs-extra": "^9.1.0", - "p-map": "^4.0.0", - "p-map-series": "^2.1.0" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, - "@lerna/timer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz", - "integrity": "sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg==", - "dev": true - }, - "@lerna/validation-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz", - "integrity": "sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw==", - "dev": true, - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/version": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz", - "integrity": "sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA==", - "dev": true, - "requires": { - "@lerna/check-working-tree": "4.0.0", - "@lerna/child-process": "4.0.0", - "@lerna/collect-updates": "4.0.0", - "@lerna/command": "4.0.0", - "@lerna/conventional-commits": "4.0.0", - "@lerna/github-client": "4.0.0", - "@lerna/gitlab-client": "4.0.0", - "@lerna/output": "4.0.0", - "@lerna/prerelease-id-from-version": "4.0.0", - "@lerna/prompt": "4.0.0", - "@lerna/run-lifecycle": "4.0.0", - "@lerna/run-topologically": "4.0.0", - "@lerna/validation-error": "4.0.0", - "chalk": "^4.1.0", - "dedent": "^0.7.0", - "load-json-file": "^6.2.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "p-map": "^4.0.0", - "p-pipe": "^3.1.0", - "p-reduce": "^2.1.0", - "p-waterfall": "^2.1.1", - "semver": "^7.3.4", - "slash": "^3.0.0", - "temp-write": "^4.0.0", - "write-json-file": "^4.3.0" - }, - "dependencies": { - "load-json-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", - "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^5.0.0", - "strip-bom": "^4.0.0", - "type-fest": "^0.6.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "@lerna/write-log-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz", - "integrity": "sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg==", - "dev": true, - "requires": { - "npmlog": "^4.1.2", - "write-file-atomic": "^3.0.3" - } - }, - "@lifterlms/brand": { - "version": "file:packages/brand", - "dev": true - }, - "@lifterlms/dev": { - "version": "file:packages/dev", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "columnify": "^1.5.4", - "commander": "^8.2.0", - "inquirer": "^8.2.0", - "replace-in-file": "^6.3.1", - "semver": "^7.3.5", - "yaml": "^1.10.2" - } - }, - "@lifterlms/llms-e2e-test-utils": { - "version": "file:packages/llms-e2e-test-utils", - "dev": true, - "requires": { - "@wordpress/e2e-test-utils": "^6.0.0", - "css-xpath": "^1.0.0", - "semver": "^7.3.5" - } - }, - "@lifterlms/scripts": { - "version": "file:packages/scripts", - "dev": true, - "requires": { - "@jest/test-sequencer": "^27.4.6", - "@wordpress/scripts": "^20.0.2", - "dotenv": "^8.2.0", - "webpack-rtl-plugin": "^2.0.0" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - } - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@npmcli/ci-detect": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz", - "integrity": "sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q==", - "dev": true - }, - "@npmcli/fs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.0.0.tgz", - "integrity": "sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ==", - "dev": true, - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "@npmcli/git": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", - "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", - "dev": true, - "requires": { - "@npmcli/promise-spawn": "^1.3.2", - "lru-cache": "^6.0.0", - "mkdirp": "^1.0.4", - "npm-pick-manifest": "^6.1.1", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^2.0.2" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@npmcli/installed-package-contents": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", - "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", - "dev": true, - "requires": { - "npm-bundled": "^1.1.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "dev": true, - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "@npmcli/node-gyp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz", - "integrity": "sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA==", - "dev": true - }, - "@npmcli/promise-spawn": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz", - "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==", - "dev": true, - "requires": { - "infer-owner": "^1.0.4" - } - }, - "@npmcli/run-script": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.6.tgz", - "integrity": "sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g==", - "dev": true, - "requires": { - "@npmcli/node-gyp": "^1.0.2", - "@npmcli/promise-spawn": "^1.3.2", - "node-gyp": "^7.1.0", - "read-package-json-fast": "^2.0.1" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "node-gyp": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", - "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "request": "^2.88.2", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.0.2", - "which": "^2.0.2" - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "dev": true, - "requires": { - "@octokit/types": "^6.0.3" - } - }, - "@octokit/core": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", - "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", - "dev": true, - "requires": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.0", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dev": true, - "requires": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - } - } - }, - "@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "dev": true, - "requires": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==", - "dev": true - }, - "@octokit/plugin-enterprise-rest": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz", - "integrity": "sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==", - "dev": true - }, - "@octokit/plugin-paginate-rest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", - "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", - "dev": true, - "requires": { - "@octokit/types": "^6.34.0" - } - }, - "@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", - "dev": true - }, - "@octokit/plugin-rest-endpoint-methods": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", - "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", - "dev": true, - "requires": { - "@octokit/types": "^6.34.0", - "deprecation": "^2.3.1" - } - }, - "@octokit/request": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz", - "integrity": "sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA==", - "dev": true, - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.1", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - } - } - }, - "@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dev": true, - "requires": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/rest": { - "version": "18.12.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", - "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", - "dev": true, - "requires": { - "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.8", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.12.0" - } - }, - "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", - "dev": true, - "requires": { - "@octokit/openapi-types": "^11.2.0" - } - }, - "@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz", - "integrity": "sha512-zZbZeHQDnoTlt2AF+diQT0wsSXpvWiaIOZwBRdltNFhG1+I3ozyaw7U/nBiUwyJ0D+zwdXp0E3bWOl38Ag2BMw==", - "dev": true, - "requires": { - "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", - "core-js-pure": "^3.8.1", - "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", - "dev": true - }, - "@romainberger/css-diff": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha1-ztOHU11PQqQqwf4TwJ3pf1rhNEw=", - "dev": true, - "requires": { - "lodash.merge": "^4.4.0", - "postcss": "^5.0.21" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "postcss": { - "version": "5.2.18", - "resolved": "", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "@sideway/address": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", - "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true - }, - "@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true - }, - "@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true - }, - "@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true - }, - "@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true - }, - "@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true - }, - "@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true - }, - "@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true - }, - "@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dev": true, - "requires": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - } - }, - "@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dev": true, - "requires": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dev": true, - "requires": { - "@babel/types": "^7.12.6" - } - }, - "@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - } - }, - "@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - } - }, - "@tannin/compile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", - "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", - "dev": true, - "requires": { - "@tannin/evaluate": "^1.2.0", - "@tannin/postfix": "^1.1.0" - } - }, - "@tannin/evaluate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", - "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", - "dev": true - }, - "@tannin/plural-forms": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", - "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", - "dev": true, - "requires": { - "@tannin/compile": "^1.1.0" - } - }, - "@tannin/postfix": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", - "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", - "dev": true - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true - }, - "@types/babel__core": { - "version": "7.1.18", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz", - "integrity": "sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/cheerio": { - "version": "0.22.31", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.31.tgz", - "integrity": "sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/http-proxy": { - "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", - "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", - "dev": true, - "requires": { - "@types/unist": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/minimist": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", - "dev": true - }, - "@types/node": { - "version": "13.1.6", - "resolved": "", - "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/prettier": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz", - "integrity": "sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==", - "dev": true - }, - "@types/prop-types": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true - }, - "@types/q": { - "version": "1.5.4", - "resolved": "", - "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", - "dev": true - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "@types/react": { - "version": "17.0.39", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz", - "integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", - "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", - "dev": true - }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true - }, - "@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true - }, - "@types/uglify-js": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", - "integrity": "sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@types/unist": { - "version": "2.0.3", - "resolved": "", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", - "dev": true - }, - "@types/vfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", - "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/unist": "*", - "@types/vfile-message": "*" - } - }, - "@types/vfile-message": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz", - "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==", - "dev": true, - "requires": { - "vfile-message": "*" - } - }, - "@types/webpack": { - "version": "4.41.32", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", - "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "@types/ws": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz", - "integrity": "sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", - "dev": true - }, - "@types/yauzl": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", - "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz", - "integrity": "sha512-fwCMkDimwHVeIOKeBHiZhRUfJXU8n6xW1FL9diDxAyGAFvKcH4csy0v7twivOQdQdA0KC8TDr7GGRd3L4Lv0rQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.12.0", - "@typescript-eslint/type-utils": "5.12.0", - "@typescript-eslint/utils": "5.12.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@typescript-eslint/experimental-utils": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.12.0.tgz", - "integrity": "sha512-iFVADWH2CmiDF+E9kFK2r474BO2JILDKw1NVD5ytqHrM3ezsfdu5uo6B+77DH0suM7iUC/yOayHNziuiI9BPbQ==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.12.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.12.0.tgz", - "integrity": "sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.12.0", - "@typescript-eslint/types": "5.12.0", - "@typescript-eslint/typescript-estree": "5.12.0", - "debug": "^4.3.2" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz", - "integrity": "sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.12.0", - "@typescript-eslint/visitor-keys": "5.12.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.12.0.tgz", - "integrity": "sha512-9j9rli3zEBV+ae7rlbBOotJcI6zfc6SHFMdKI9M3Nc0sy458LJ79Os+TPWeBBL96J9/e36rdJOfCuyRSgFAA0Q==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "5.12.0", - "debug": "^4.3.2", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@typescript-eslint/types": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.12.0.tgz", - "integrity": "sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz", - "integrity": "sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.12.0", - "@typescript-eslint/visitor-keys": "5.12.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.12.0.tgz", - "integrity": "sha512-k4J2WovnMPGI4PzKgDtQdNrCnmBHpMUFy21qjX2CoPdoBcSBIMvVBr9P2YDP8jOqZOeK3ThOL6VO/sy6jtnvzw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.12.0", - "@typescript-eslint/types": "5.12.0", - "@typescript-eslint/typescript-estree": "5.12.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz", - "integrity": "sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.12.0", - "eslint-visitor-keys": "^3.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - } - } - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webpack-cli/configtest": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", - "dev": true - }, - "@webpack-cli/info": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", - "dev": true, - "requires": { - "envinfo": "^7.7.3" - } - }, - "@webpack-cli/serve": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", - "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", - "dev": true - }, - "@wojtekmaj/enzyme-adapter-react-17": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.6.tgz", - "integrity": "sha512-gSfhg8CiL0Vwc2UgUblGVZIy7M0KyXaZsd8+QwzV8TSVRLkGyzdLtYEcs9wRWyQTsdmOd+oRGqbVgUX7AVJxug==", - "dev": true, - "requires": { - "@wojtekmaj/enzyme-adapter-utils": "^0.1.2", - "enzyme-shallow-equal": "^1.0.0", - "has": "^1.0.0", - "prop-types": "^15.7.0", - "react-is": "^17.0.0", - "react-test-renderer": "^17.0.0" - } - }, - "@wojtekmaj/enzyme-adapter-utils": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.4.tgz", - "integrity": "sha512-ARGIQSIIv3oBia1m5Ihn1VU0FGmft6KPe39SBKTb8p7LSXO23YI4kNtc4M/cKoIY7P+IYdrZcgMObvedyjoSQA==", - "dev": true, - "requires": { - "function.prototype.name": "^1.1.0", - "has": "^1.0.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.0" - } - }, - "@wordpress/api-fetch": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.0.1.tgz", - "integrity": "sha512-rodFmGcnhI5gLKRueabLHiNrPpl/i+DCD23xg8/xs2Iyr47YFZZN4KB8WKaRVDxPZQJAB67IqMLs/h4U02HdmA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.3.1", - "@wordpress/url": "^3.4.1" - } - }, - "@wordpress/babel-plugin-import-jsx-pragma": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-3.1.1.tgz", - "integrity": "sha512-0gopMgFMVBtJiYTwsxq4ERhbeHV2iI11fHt52fguBr8eS7h61ufp3uZy0aapdh8vQh80S2v1rgdYJwAWb73r1w==", - "dev": true - }, - "@wordpress/babel-preset-default": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-6.5.1.tgz", - "integrity": "sha512-mfCLHQe7emZoxR9PQBRnBRYIBvO2Z2SsCrVRjk5sUg0iPv5TH03Sox1Gn3/WTKat2p8UU08en865UG0OYhGgeA==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@babel/plugin-transform-react-jsx": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.0", - "@babel/preset-env": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.0", - "@wordpress/babel-plugin-import-jsx-pragma": "^3.1.1", - "@wordpress/browserslist-config": "^4.1.1", - "@wordpress/element": "^4.1.1", - "@wordpress/warning": "^2.3.1", - "browserslist": "^4.17.6", - "core-js": "^3.19.1" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/core": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.2.tgz", - "integrity": "sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.0.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.2", - "@babel/parser": "^7.17.0", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", - "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@wordpress/base-styles": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-4.1.1.tgz", - "integrity": "sha512-DFWyfgHxsVhKqRZH4cCrQjItNMDIoPBaJd3/bTd2jPCu+MN/PITR1xUY6cET3ASCR34vrJ0fWZeylncrCixTnw==", - "dev": true - }, - "@wordpress/browserslist-config": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-4.1.1.tgz", - "integrity": "sha512-fz2IQ3eghmnWIb3YnSSC1aNlrdNPBF53b5RIdF6Zt5Wtk9k3NZ+YmH6ph8zUyktSzckRkV0dNsI3X9Z1RU49gQ==", - "dev": true - }, - "@wordpress/dependency-extraction-webpack-plugin": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-3.3.1.tgz", - "integrity": "sha512-BQcjilKXgHWUWmzjzSwtE5Dw8IUmjaXtSAxlh988CBfNRk94BSSTI4FqXH1R0ywpAF6ebb4NTP+9Y6joydENVA==", - "dev": true, - "requires": { - "json2php": "^0.0.4", - "webpack-sources": "^3.2.2" - } - }, - "@wordpress/docgen": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/docgen/-/docgen-1.18.0.tgz", - "integrity": "sha512-C2cnKW24S+JFn8shFBPxbkeV1VZyfLNFKVq4nFSx0yjmZKAraHXj5y179xmk/c2z4UlGyAtwS95BJOYvcdr6Hg==", - "dev": true, - "requires": { - "@babel/core": "^7.13.10", - "comment-parser": "^1.1.1", - "lodash": "^4.17.21", - "mdast-util-inject": "1.1.0", - "optionator": "0.8.2", - "remark": "10.0.1", - "remark-parse": "6.0.3", - "unified": "7.1.0" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "markdown-table": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", - "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", - "dev": true - }, - "mdast-util-compact": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", - "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "remark": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz", - "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==", - "dev": true, - "requires": { - "remark-parse": "^6.0.0", - "remark-stringify": "^6.0.0", - "unified": "^7.0.0" - } - }, - "remark-parse": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", - "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remark-stringify": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", - "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", - "dev": true, - "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^1.1.0", - "mdast-util-compact": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^1.0.1", - "unherit": "^1.0.4", - "xtend": "^4.0.1" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", - "dev": true, - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "unified": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", - "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "@types/vfile": "^3.0.0", - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^3.0.0", - "x-is-string": "^0.1.0" - } - }, - "vfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", - "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", - "dev": true, - "requires": { - "is-buffer": "^2.0.0", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "@wordpress/e2e-test-utils": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-6.0.1.tgz", - "integrity": "sha512-8UwslYwGqgS1Kv5ZKj9fXdxuL9Sl2YyBAYBCS0BJe5fbSYL7R7VvbVUTksLM5daV+o3yh5Xn80vHhFb1nV7+wg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "^6.0.1", - "@wordpress/keycodes": "^3.3.1", - "@wordpress/url": "^3.4.1", - "form-data": "^4.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.6.0" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "@wordpress/element": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-4.1.1.tgz", - "integrity": "sha512-lzrCvQOtzyRguw+VlG8EVzy8aexJ2Fk3tN8ifefvvTN0vNJeFdoEaSZGqYCoVvRKHKXpLfX9vMsVp0Ug0EWPcQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "@types/react": "^17.0.37", - "@types/react-dom": "^17.0.11", - "@wordpress/escape-html": "^2.3.1", - "lodash": "^4.17.21", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, - "@wordpress/escape-html": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.3.1.tgz", - "integrity": "sha512-7lqbW1NiIQOlAwxc6iAfZ69v7sf2/2lZOfS5ntIrdB+erqHURkgwvqAOGJ2VwJcjQadaKbDIMf8YPZPwYY66+A==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0" - } - }, - "@wordpress/eslint-plugin": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-10.0.1.tgz", - "integrity": "sha512-MsmuEckT7pUWccky0cZDAfeFGPLWuRJwlf3GSWEPCGjCxzxoxQEXTj6GS21UoRPat8SyuKIx8d/8Da1SQM01UQ==", - "dev": true, - "requires": { - "@babel/eslint-parser": "^7.16.0", - "@typescript-eslint/eslint-plugin": "^5.3.0", - "@typescript-eslint/parser": "^5.3.0", - "@wordpress/babel-preset-default": "^6.5.1", - "@wordpress/prettier-config": "^1.1.2", - "cosmiconfig": "^7.0.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.3", - "eslint-plugin-jsdoc": "^37.0.3", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^3.3.0", - "eslint-plugin-react": "^7.27.0", - "eslint-plugin-react-hooks": "^4.3.0", - "globals": "^13.12.0", - "prettier": "npm:wp-prettier@2.2.1-beta-1", - "requireindex": "^1.2.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } - } - }, - "@wordpress/hooks": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.3.1.tgz", - "integrity": "sha512-Eyc5YYX8010Ihr6Ab4lq9G7J9DmPiLnGf6C4WwEMf0iQ9SBw8hcp2TCwkSyC12DU7iY0o11FYGfgdGW3UpPiYA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0" - } - }, - "@wordpress/i18n": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.3.1.tgz", - "integrity": "sha512-4xeGUOhZL+Xl97VPSEslWJxCjQPuK2I8AEJWe5cb1u/YOcgTzOav2QN7T7wYzt3Os5bfqNBmTFMfOr+1goFPrw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.3.1", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/jest-console": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-5.0.1.tgz", - "integrity": "sha512-xVYGtzsewfI5QhdIX9Sm+aqUZTJYuWYEFVBAhJJdEGwAeqirZPEADZJjZwh3olOTvHBVk+YpMhBaExjxVSUt9g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "jest-matcher-utils": "^27.4.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/jest-preset-default": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-8.0.1.tgz", - "integrity": "sha512-nqVlXo3XAwDlVCVbpPuYIXTDuMa9X3n3Vz6i9cVVaM9jO1/mb1s6d2sGCUTBFuipHFkr615OV0f/ZRKBmypR0Q==", - "dev": true, - "requires": { - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", - "@wordpress/jest-console": "^5.0.1", - "babel-jest": "^27.4.5", - "enzyme": "^3.11.0", - "enzyme-to-json": "^3.4.4" - } - }, - "@wordpress/keycodes": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.3.1.tgz", - "integrity": "sha512-d/8wCjqB8c5426i8kLSRDxR/tezZFR0R/OOPFLoh/XnKiuRda/8OuPdSTUDmsLCam+yjV9K5uRm0KcMiZeFu7A==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.3.1", - "lodash": "^4.17.21" - } - }, - "@wordpress/npm-package-json-lint-config": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-4.1.1.tgz", - "integrity": "sha512-Kuyge30wO3qceDTg3PRrF/lPP4h4f7gwJMTkFFv68Ouvv6HBPubjfAVHtrtFOaFkxMnt91oqOXLAL1y7j95L4w==", - "dev": true - }, - "@wordpress/postcss-plugins-preset": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-3.3.1.tgz", - "integrity": "sha512-UEznsalMpDetECLKjUbHw+CQ5YkDtygrnHazZlHViAU0yLOOCru5YdUZy9NulabFOTZk9nhHPIul+u/1l93rNg==", - "dev": true, - "requires": { - "@wordpress/base-styles": "^4.1.1", - "autoprefixer": "^10.2.5" - }, - "dependencies": { - "autoprefixer": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz", - "integrity": "sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==", - "dev": true, - "requires": { - "browserslist": "^4.19.1", - "caniuse-lite": "^1.0.30001297", - "fraction.js": "^4.1.2", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "@wordpress/prettier-config": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-1.1.2.tgz", - "integrity": "sha512-jVUd22QAErCxdYsP33HC10GLDMbLU6A1bYgRpA//VxirJwvbT/chEnkO9Xy2TILXYtpil4WJtGoD9Fv599N82Q==", - "dev": true - }, - "@wordpress/scripts": { - "version": "20.0.2", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-20.0.2.tgz", - "integrity": "sha512-6BHOAUmZ0XyjhayD5gKUuXsEpOEI8oZhcTJ85GTbib5TGBXd8VxTuh8b53fOOppbqh7YlWSSSsuMX3HaAfUYeg==", - "dev": true, - "requires": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.2", - "@svgr/webpack": "^5.5.0", - "@wordpress/babel-preset-default": "^6.5.0", - "@wordpress/browserslist-config": "^4.1.0", - "@wordpress/dependency-extraction-webpack-plugin": "^3.3.0", - "@wordpress/eslint-plugin": "^10.0.0", - "@wordpress/jest-preset-default": "^8.0.0", - "@wordpress/npm-package-json-lint-config": "^4.1.0", - "@wordpress/postcss-plugins-preset": "^3.3.0", - "@wordpress/prettier-config": "^1.1.1", - "@wordpress/stylelint-config": "^20.0.0", - "adm-zip": "^0.5.9", - "babel-jest": "^27.4.5", - "babel-loader": "^8.2.3", - "browserslist": "^4.17.6", - "chalk": "^4.0.0", - "check-node-version": "^4.1.0", - "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^10.2.0", - "cross-spawn": "^5.1.0", - "css-loader": "^6.2.0", - "cssnano": "^5.0.7", - "cwd": "^0.10.0", - "dir-glob": "^3.0.1", - "eslint": "^8.3.0", - "eslint-plugin-markdown": "^2.2.0", - "expect-puppeteer": "^4.4.0", - "fast-glob": "^3.2.7", - "filenamify": "^4.2.0", - "jest": "^27.4.5", - "jest-dev-server": "^6.0.2", - "jest-environment-node": "^27.4.4", - "markdownlint": "^0.23.1", - "markdownlint-cli": "^0.27.1", - "merge-deep": "^3.0.3", - "mini-css-extract-plugin": "^2.5.1", - "minimist": "^1.2.0", - "npm-package-json-lint": "^5.0.0", - "npm-packlist": "^3.0.0", - "postcss": "^8.4.5", - "postcss-loader": "^6.2.1", - "prettier": "npm:wp-prettier@2.2.1-beta-1", - "puppeteer-core": "^11.0.0", - "react-refresh": "^0.10.0", - "read-pkg-up": "^7.0.1", - "resolve-bin": "^0.4.0", - "sass": "^1.35.2", - "sass-loader": "^12.1.0", - "source-map-loader": "^3.0.0", - "stylelint": "^14.2.0", - "terser-webpack-plugin": "^5.1.4", - "url-loader": "^4.1.1", - "webpack": "^5.47.1", - "webpack-bundle-analyzer": "^4.4.2", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.4.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/core": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.2.tgz", - "integrity": "sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.0.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.2", - "@babel/parser": "^7.17.0", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", - "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "css-declaration-sorter": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz", - "integrity": "sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw==", - "dev": true, - "requires": { - "timsort": "^0.3.0" - } - }, - "css-select": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", - "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^5.1.0", - "domhandler": "^4.3.0", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", - "dev": true - }, - "cssnano": { - "version": "5.0.17", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.17.tgz", - "integrity": "sha512-fmjLP7k8kL18xSspeXTzRhaFtRI7DL9b8IcXR80JgtnWBpvAzHT7sCR/6qdn0tnxIaINUN6OEQu83wF57Gs3Xw==", - "dev": true, - "requires": { - "cssnano-preset-default": "^5.1.12", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - } - }, - "cssnano-preset-default": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.12.tgz", - "integrity": "sha512-rO/JZYyjW1QNkWBxMGV28DW7d98UDLaF759frhli58QFehZ+D/LSmwQ2z/ylBAe2hUlsIWTq6NYGfQPq65EF9w==", - "dev": true, - "requires": { - "css-declaration-sorter": "^6.0.3", - "cssnano-utils": "^3.0.2", - "postcss-calc": "^8.2.0", - "postcss-colormin": "^5.2.5", - "postcss-convert-values": "^5.0.4", - "postcss-discard-comments": "^5.0.3", - "postcss-discard-duplicates": "^5.0.3", - "postcss-discard-empty": "^5.0.3", - "postcss-discard-overridden": "^5.0.4", - "postcss-merge-longhand": "^5.0.6", - "postcss-merge-rules": "^5.0.6", - "postcss-minify-font-values": "^5.0.4", - "postcss-minify-gradients": "^5.0.6", - "postcss-minify-params": "^5.0.5", - "postcss-minify-selectors": "^5.1.3", - "postcss-normalize-charset": "^5.0.3", - "postcss-normalize-display-values": "^5.0.3", - "postcss-normalize-positions": "^5.0.4", - "postcss-normalize-repeat-style": "^5.0.4", - "postcss-normalize-string": "^5.0.4", - "postcss-normalize-timing-functions": "^5.0.3", - "postcss-normalize-unicode": "^5.0.4", - "postcss-normalize-url": "^5.0.5", - "postcss-normalize-whitespace": "^5.0.4", - "postcss-ordered-values": "^5.0.5", - "postcss-reduce-initial": "^5.0.3", - "postcss-reduce-transforms": "^5.0.4", - "postcss-svgo": "^5.0.4", - "postcss-unique-selectors": "^5.0.4" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "ignore-walk": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", - "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - }, - "npm-packlist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", - "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", - "dev": true, - "requires": { - "glob": "^7.1.6", - "ignore-walk": "^4.0.1", - "npm-bundled": "^1.1.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "postcss": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", - "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", - "dev": true, - "requires": { - "nanoid": "^3.2.0", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-colormin": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.5.tgz", - "integrity": "sha512-+X30aDaGYq81mFqwyPpnYInsZQnNpdxMX0ajlY7AExCexEFkPVV+KrO7kXwayqEWL2xwEbNQ4nUO0ZsRWGnevg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-convert-values": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.4.tgz", - "integrity": "sha512-bugzSAyjIexdObovsPZu/sBCTHccImJxLyFgeV0MmNBm/Lw5h5XnjfML6gzEmJ3A6nyfCW7hb1JXzcsA4Zfbdw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-discard-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.3.tgz", - "integrity": "sha512-6W5BemziRoqIdAKT+1QjM4bNcJAQ7z7zk073730NHg4cUXh3/rQHHj7pmYxUB9aGhuRhBiUf0pXvIHkRwhQP0Q==", - "dev": true - }, - "postcss-discard-duplicates": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.3.tgz", - "integrity": "sha512-vPtm1Mf+kp7iAENTG7jI1MN1lk+fBqL5y+qxyi4v3H+lzsXEdfS3dwUZD45KVhgzDEgduur8ycB4hMegyMTeRw==", - "dev": true - }, - "postcss-discard-empty": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.3.tgz", - "integrity": "sha512-xGJugpaXKakwKI7sSdZjUuN4V3zSzb2Y0LOlmTajFbNinEjTfVs9PFW2lmKBaC/E64WwYppfqLD03P8l9BuueA==", - "dev": true - }, - "postcss-discard-overridden": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.4.tgz", - "integrity": "sha512-3j9QH0Qh1KkdxwiZOW82cId7zdwXVQv/gRXYDnwx5pBtR1sTkU4cXRK9lp5dSdiM0r0OICO/L8J6sV1/7m0kHg==", - "dev": true - }, - "postcss-merge-longhand": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.6.tgz", - "integrity": "sha512-rkmoPwQO6ymJSmWsX6l2hHeEBQa7C4kJb9jyi5fZB1sE8nSCv7sqchoYPixRwX/yvLoZP2y6FA5kcjiByeJqDg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.0.3" - } - }, - "postcss-merge-rules": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.6.tgz", - "integrity": "sha512-nzJWJ9yXWp8AOEpn/HFAW72WKVGD2bsLiAmgw4hDchSij27bt6TF+sIK0cJUBAYT3SGcjtGGsOR89bwkkMuMgQ==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.0.2", - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-minify-font-values": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.4.tgz", - "integrity": "sha512-RN6q3tyuEesvyCYYFCRGJ41J1XFvgV+dvYGHr0CeHv8F00yILlN8Slf4t8XW4IghlfZYCeyRrANO6HpJ948ieA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-gradients": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.6.tgz", - "integrity": "sha512-E/dT6oVxB9nLGUTiY/rG5dX9taugv9cbLNTFad3dKxOO+BQg25Q/xo2z2ddG+ZB1CbkZYaVwx5blY8VC7R/43A==", - "dev": true, - "requires": { - "colord": "^2.9.1", - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-params": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.5.tgz", - "integrity": "sha512-YBNuq3Rz5LfLFNHb9wrvm6t859b8qIqfXsWeK7wROm3jSKNpO1Y5e8cOyBv6Acji15TgSrAwb3JkVNCqNyLvBg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-selectors": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.3.tgz", - "integrity": "sha512-9RJfTiQEKA/kZhMaEXND893nBqmYQ8qYa/G+uPdVnXF6D/FzpfI6kwBtWEcHx5FqDbA79O9n6fQJfrIj6M8jvQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-normalize-charset": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.3.tgz", - "integrity": "sha512-iKEplDBco9EfH7sx4ut7R2r/dwTnUqyfACf62Unc9UiyFuI7uUqZZtY+u+qp7g8Qszl/U28HIfcsI3pEABWFfA==", - "dev": true - }, - "postcss-normalize-display-values": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.3.tgz", - "integrity": "sha512-FIV5FY/qs4Ja32jiDb5mVj5iWBlS3N8tFcw2yg98+8MkRgyhtnBgSC0lxU+16AMHbjX5fbSJgw5AXLMolonuRQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-positions": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.4.tgz", - "integrity": "sha512-qynirjBX0Lc73ROomZE3lzzmXXTu48/QiEzKgMeqh28+MfuHLsuqC9po4kj84igZqqFGovz8F8hf44hA3dPYmQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-repeat-style": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.4.tgz", - "integrity": "sha512-Innt+wctD7YpfeDR7r5Ik6krdyppyAg2HBRpX88fo5AYzC1Ut/l3xaxACG0KsbX49cO2n5EB13clPwuYVt8cMA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-string": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.4.tgz", - "integrity": "sha512-Dfk42l0+A1CDnVpgE606ENvdmksttLynEqTQf5FL3XGQOyqxjbo25+pglCUvziicTxjtI2NLUR6KkxyUWEVubQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-timing-functions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.3.tgz", - "integrity": "sha512-QRfjvFh11moN4PYnJ7hia4uJXeFotyK3t2jjg8lM9mswleGsNw2Lm3I5wO+l4k1FzK96EFwEVn8X8Ojrp2gP4g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-unicode": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.4.tgz", - "integrity": "sha512-W79Regn+a+eXTzB+oV/8XJ33s3pDyFTND2yDuUCo0Xa3QSy1HtNIfRVPXNubHxjhlqmMFADr3FSCHT84ITW3ig==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-url": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.5.tgz", - "integrity": "sha512-Ws3tX+PcekYlXh+ycAt0wyzqGthkvVtZ9SZLutMVvHARxcpu4o7vvXcNoiNKyjKuWecnjS6HDI3fjBuDr5MQxQ==", - "dev": true, - "requires": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-whitespace": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.4.tgz", - "integrity": "sha512-wsnuHolYZjMwWZJoTC9jeI2AcjA67v4UuidDrPN9RnX8KIZfE+r2Nd6XZRwHVwUiHmRvKQtxiqo64K+h8/imaw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-ordered-values": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.5.tgz", - "integrity": "sha512-mfY7lXpq+8bDEHfP+muqibDPhZ5eP9zgBEF9XRvoQgXcQe2Db3G1wcvjbnfjXG6wYsl+0UIjikqq4ym1V2jGMQ==", - "dev": true, - "requires": { - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-reduce-initial": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.3.tgz", - "integrity": "sha512-c88TkSnQ/Dnwgb4OZbKPOBbCaauwEjbECP5uAuFPOzQ+XdjNjRH7SG0dteXrpp1LlIFEKK76iUGgmw2V0xeieA==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.4.tgz", - "integrity": "sha512-VIJB9SFSaL8B/B7AXb7KHL6/GNNbbCHslgdzS9UDfBZYIA2nx8NLY7iD/BXFSO/1sRUILzBTfHCoW5inP37C5g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-selector-parser": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz", - "integrity": "sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.4.tgz", - "integrity": "sha512-yDKHvULbnZtIrRqhZoA+rxreWpee28JSRH/gy9727u0UCgtpv1M/9WEWY3xySlFa0zQJcqf6oCBJPR5NwkmYpg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - } - }, - "postcss-unique-selectors": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.4.tgz", - "integrity": "sha512-5ampwoSDJCxDPoANBIlMgoBcYUHnhaiuLYJR5pj1DLnYQvMRVyFuTA5C3Bvt+aHtiqWpJkD/lXT50Vo1D0ZsAQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "stylehacks": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.3.tgz", - "integrity": "sha512-ENcUdpf4yO0E1rubu8rkxI+JGQk4CgjchynZ4bDBJDfqdy+uhTRSWb8/F3Jtu+Bw5MW45Po3/aQGeIyyxgQtxg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-selector-parser": "^6.0.4" - } - }, - "svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "requires": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "@wordpress/stylelint-config": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-20.0.1.tgz", - "integrity": "sha512-f+aydCTrfFcEvx0eOS4N1VRV8MSl/zZTIPJcPkh2oV1yLNq0pL4zQ5OYMlSg5vaj0yZJdRLyGVa+VSjy+D81Ag==", - "dev": true, - "requires": { - "stylelint-config-recommended": "^6.0.0", - "stylelint-config-recommended-scss": "^5.0.2" - } - }, - "@wordpress/url": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.4.1.tgz", - "integrity": "sha512-EeE/qCTe2wYxvEhH4ygV8CONX7j1aQaZF5LUg+QHZ+cnV5Bo8SkcLZdOHqczwvljqwVnKc+ybzQx/WLE+APNSw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "lodash": "^4.17.21" - } - }, - "@wordpress/warning": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.3.1.tgz", - "integrity": "sha512-cnQaWv3IUuFSdZ/5xR6yabOYS5KJV7r3qSzh5CTdl4b9B6jXlVHzcqmRAz+up7+qxF4awmkJhqlozwz9TBCyjg==", - "dev": true - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "dependencies": { - "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true - }, - "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "requires": { - "mime-db": "1.51.0" - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - } - } - }, - "acorn": { - "version": "5.7.4", - "resolved": "", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "add-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", - "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", - "dev": true - }, - "adm-zip": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", - "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "agentkeepalive": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz", - "integrity": "sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", - "humanize-ms": "^1.2.1" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "requires": { - "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } - }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, - "any-shell-escape": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-1Vq5ciRMcaml4asIefML8RCAaVk=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "append-buffer": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", - "dev": true, - "requires": { - "buffer-equal": "^1.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "archy": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-filter": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-map": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-union": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", - "dev": true - }, - "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - } - }, - "array-initial": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-last": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-slice": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-sort": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.filter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.1.tgz", - "integrity": "sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - } - }, - "array.prototype.flat": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" - } - }, - "array.prototype.flatmap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", - "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "async-done": { - "version": "1.3.1", - "resolved": "", - "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^1.0.7", - "stream-exhaust": "^1.0.1" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - } - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "async-settle": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true, - "requires": { - "async-done": "^1.2.2" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, - "atob": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", - "dev": true - }, - "autoprefixer": { - "version": "8.6.5", - "resolved": "", - "integrity": "sha512-PLWJN3Xo/rycNkx+mp8iBDMTm3FeWe4VmYaZDSqL5QQB9sLsQkG5k8n+LNDFnhh9kdq2K+egL/icpctOmDHwig==", - "dev": true, - "requires": { - "browserslist": "^3.2.8", - "caniuse-lite": "^1.0.30000864", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^6.0.23", - "postcss-value-parser": "^3.2.3" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "aws-sdk": { - "version": "2.632.0", - "resolved": "", - "integrity": "sha512-8Ewnxpi1jWN/nTc4ngDqeAiReqlib0SfIPQFHNozyJFOdOW6ERKd/hGdrci9qXJIn8NYQj82QsR3JTjjFzG9Zg==", - "dev": true, - "requires": { - "buffer": "4.9.1", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "ieee754": { - "version": "1.1.13", - "resolved": "", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.10.0", - "resolved": "", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", - "dev": true - }, - "axe-core": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", - "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", - "dev": true - }, - "axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.7" - } - }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dev": true, - "requires": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "babel-loader": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", - "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", - "dev": true, - "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^1.4.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", - "semver": "^6.1.1" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.21.0" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "bach": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - } - }, - "bail": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "beeper": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, - "before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "1.12.0", - "resolved": "", - "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", - "dev": true - }, - "binaryextensions": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", - "dev": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", - "dev": true, - "requires": { - "bytes": "3.1.1", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.6", - "raw-body": "2.4.2", - "type-is": "~1.6.18" - }, - "dependencies": { - "bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", - "dev": true - }, - "qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", - "dev": true - } - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browserslist": { - "version": "3.2.8", - "resolved": "", - "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", - "dev": true - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "byte-size": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-7.0.1.tgz", - "integrity": "sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A==", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dev": true, - "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - } - } - }, - "caniuse-lite": { - "version": "1.0.30001271", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz", - "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "ccount": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "character-entities": { - "version": "1.2.4", - "resolved": "", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "dev": true - }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true - }, - "character-entities-legacy": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "dev": true - }, - "character-reference-invalid": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "dev": true - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-node-version": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.2.1.tgz", - "integrity": "sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "map-values": "^1.0.1", - "minimist": "^1.2.0", - "object-filter": "^1.0.2", - "run-parallel": "^1.1.4", - "semver": "^6.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", - "dev": true, - "requires": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } - } - }, - "cheerio-select": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", - "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", - "dev": true, - "requires": { - "css-select": "^4.1.3", - "css-what": "^5.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0", - "domutils": "^2.7.0" - }, - "dependencies": { - "css-select": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", - "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^5.1.0", - "domhandler": "^4.3.0", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", - "dev": true - }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - } - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "clean-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", - "dev": true, - "requires": { - "@types/webpack": "^4.4.31", - "del": "^4.1.1" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true - }, - "cli-width": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-deep": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", - "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", - "dev": true, - "requires": { - "for-own": "^0.1.3", - "is-plain-object": "^2.0.1", - "kind-of": "^3.0.2", - "lazy-cache": "^1.0.3", - "shallow-clone": "^0.1.2" - }, - "dependencies": { - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "clone-regexp": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", - "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", - "dev": true, - "requires": { - "is-regexp": "^2.0.0" - } - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "cmd-shim": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz", - "integrity": "sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw==", - "dev": true, - "requires": { - "mkdirp-infer-owner": "^2.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-map": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.2", - "resolved": "", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, - "requires": { - "color-name": "1.1.1" - } - }, - "color-name": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true - }, - "color-string": { - "version": "1.5.3", - "resolved": "", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true - }, - "colord": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz", - "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==", - "dev": true - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "colors": { - "version": "0.6.2", - "resolved": "", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", - "dev": true - }, - "columnify": { - "version": "1.5.4", - "resolved": "", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "dev": true, - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true - }, - "comment-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", - "integrity": "sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw==", - "dev": true - }, - "common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - }, - "dependencies": { - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - } - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "concat-with-sourcemaps": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "conventional-changelog-angular": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", - "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", - "dev": true, - "requires": { - "compare-func": "^2.0.0", - "q": "^1.5.1" - } - }, - "conventional-changelog-core": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", - "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", - "dev": true, - "requires": { - "add-stream": "^1.0.0", - "conventional-changelog-writer": "^5.0.0", - "conventional-commits-parser": "^3.2.0", - "dateformat": "^3.0.0", - "get-pkg-repo": "^4.0.0", - "git-raw-commits": "^2.0.8", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^4.1.1", - "lodash": "^4.17.15", - "normalize-package-data": "^3.0.0", - "q": "^1.5.1", - "read-pkg": "^3.0.0", - "read-pkg-up": "^3.0.0", - "through2": "^4.0.0" - }, - "dependencies": { - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "requires": { - "readable-stream": "3" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "conventional-changelog-preset-loader": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", - "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", - "dev": true - }, - "conventional-changelog-writer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz", - "integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==", - "dev": true, - "requires": { - "conventional-commits-filter": "^2.0.7", - "dateformat": "^3.0.0", - "handlebars": "^4.7.6", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "semver": "^6.0.0", - "split": "^1.0.0", - "through2": "^4.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "requires": { - "readable-stream": "3" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "conventional-commits-filter": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", - "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", - "dev": true, - "requires": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.3.tgz", - "integrity": "sha512-YyRDR7On9H07ICFpRm/igcdjIqebXbvf4Cff+Pf0BrBys1i1EOzx9iFXNlAbdrLAR8jf7bkUYkDAr8pEy0q4Pw==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.1", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "requires": { - "readable-stream": "3" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "conventional-recommended-bump": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz", - "integrity": "sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==", - "dev": true, - "requires": { - "concat-stream": "^2.0.0", - "conventional-changelog-preset-loader": "^2.3.4", - "conventional-commits-filter": "^2.0.7", - "conventional-commits-parser": "^3.2.0", - "git-raw-commits": "^2.0.8", - "git-semver-tags": "^4.1.1", - "meow": "^8.0.0", - "q": "^1.5.1" - }, - "dependencies": { - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-props": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", - "dev": true, - "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" - } - }, - "copy-webpack-plugin": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", - "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", - "dev": true, - "requires": { - "fast-glob": "^3.2.7", - "glob-parent": "^6.0.1", - "globby": "^12.0.2", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "array-union": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", - "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globby": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", - "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", - "dev": true, - "requires": { - "array-union": "^3.0.1", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.7", - "ignore": "^5.1.9", - "merge2": "^1.4.1", - "slash": "^4.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - }, - "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "core-js": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.0.tgz", - "integrity": "sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ==", - "dev": true - }, - "core-js-compat": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.0.tgz", - "integrity": "sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A==", - "dev": true, - "requires": { - "browserslist": "^4.19.1", - "semver": "7.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, - "core-js-pure": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.0.tgz", - "integrity": "sha512-VaJUunCZLnxuDbo1rNOzwbet9E1K9joiXS5+DQMPtgxd24wfsZbJZMMfQLGYMlCUvSxLfsRUUhoOR2x28mFfeg==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "css": { - "version": "2.2.3", - "resolved": "", - "integrity": "sha512-0W171WccAjQGGTKLhw4m2nnl0zPHUlTO/I8td4XzJgIB8Hg3ZZx71qT4G4eX8OVsSiaAKiUMy73E3nsbPlg2DQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "source-map": "^0.1.38", - "source-map-resolve": "^0.5.1", - "urix": "^0.1.0" - }, - "dependencies": { - "source-map": { - "version": "0.1.43", - "resolved": "", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "css-functions-list": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.0.1.tgz", - "integrity": "sha512-PriDuifDt4u4rkDgnqRCLnjfMatufLmWNfQnGCq34xZwpY3oabwhB9SqRBmuvWUgndbemCFlKqg+nO7C2q0SBw==", - "dev": true - }, - "css-loader": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.6.0.tgz", - "integrity": "sha512-FK7H2lisOixPT406s5gZM1S3l8GrfhEBT3ZiL2UX1Ng1XWs0y2GPllz/OTyvbaHe12VgQrIXIzuEGVlbUhodqg==", - "dev": true, - "requires": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.5", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" - }, - "dependencies": { - "postcss": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", - "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", - "dev": true, - "requires": { - "nanoid": "^3.2.0", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "css-select": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "css-what": { - "version": "3.3.0", - "resolved": "", - "integrity": "sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==", - "dev": true - }, - "css-xpath": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-AeMHirfMLJBisnx24tBpoSlRsYE=", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "4.1.10", - "resolved": "", - "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.7", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cssnano-preset-default": { - "version": "4.0.7", - "resolved": "", - "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.2", - "postcss-unique-selectors": "^4.0.1" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "cssnano-utils": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.0.2.tgz", - "integrity": "sha512-KhprijuQv2sP4kT92sSQwhlK3SJTbDIsxcfIEySB0O+3m9esFOai7dP9bMx5enHAh2MwarVIcnwiWoOm01RIbQ==", - "dev": true - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "requires": { - "css-tree": "^1.1.2" - }, - "dependencies": { - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "csstype": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", - "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "cwd": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", - "integrity": "sha1-FyQAaUBXwioTsM8WFix+S3p/5Wc=", - "dev": true, - "requires": { - "find-pkg": "^0.1.2", - "fs-exists-sync": "^0.1.0" - } - }, - "d": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true - }, - "dargs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", - "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - } - }, - "dateformat": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, - "deap": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-k75KYNZMvwAwes2xIPry/QTffXIchjD8QfABvvfTr80P85jv5ZcKqcoDo+vMe71nNnVnXYe8MA28weyqcf/DKw==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "debug-fabulous": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", - "dev": true, - "requires": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - } - }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "dedent": { - "version": "0.7.0", - "resolved": "", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "default-compare": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "requires": { - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "requires": { - "execa": "^5.0.0" - } - }, - "default-resolution": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "defaults": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "devtools-protocol": { - "version": "0.0.901419", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz", - "integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==", - "dev": true - }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "dir-glob": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } - } - }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", - "dev": true - } - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, - "domhandler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", - "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", - "dev": true, - "requires": { - "domelementtype": "^2.2.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - } - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "duplexer2": { - "version": "0.0.2", - "resolved": "", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9" - } - }, - "duplexify": { - "version": "3.6.0", - "resolved": "", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "end-of-stream": { - "version": "1.4.1", - "resolved": "", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "each-props": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.880", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.880.tgz", - "integrity": "sha512-iwIP/6WoeSimzUKJIQtjtpVDsK8Ir8qQCMXsUBwg+rxJR2Uh3wTNSbxoYRfs+3UWx/9MAnPIxVZCyWkm8MT0uw==", - "dev": true - }, - "emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "encoding": { - "version": "0.1.12", - "resolved": "", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "dev": true, - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz", - "integrity": "sha512-weDYmzbBygL7HzGGS26M3hGQx68vehdEg6VUmqSOaFzXExFqlnKuSvsEJCVGQHScS8CQMbrAqftT+AzzHNt/YA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "dev": true - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true - }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true - }, - "enzyme": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", - "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", - "dev": true, - "requires": { - "array.prototype.flat": "^1.2.3", - "cheerio": "^1.0.0-rc.3", - "enzyme-shallow-equal": "^1.0.1", - "function.prototype.name": "^1.1.2", - "has": "^1.0.3", - "html-element-map": "^1.2.0", - "is-boolean-object": "^1.0.1", - "is-callable": "^1.1.5", - "is-number-object": "^1.0.4", - "is-regex": "^1.0.5", - "is-string": "^1.0.5", - "is-subset": "^0.1.1", - "lodash.escape": "^4.0.1", - "lodash.isequal": "^4.5.0", - "object-inspect": "^1.7.0", - "object-is": "^1.0.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.1", - "object.values": "^1.1.1", - "raf": "^3.4.1", - "rst-selector-parser": "^2.2.3", - "string.prototype.trim": "^1.2.1" - }, - "dependencies": { - "lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", - "dev": true - } - } - }, - "enzyme-shallow-equal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", - "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", - "dev": true, - "requires": { - "has": "^1.0.3", - "object-is": "^1.1.2" - } - }, - "enzyme-to-json": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.2.tgz", - "integrity": "sha512-Ynm6Z6R6iwQ0g2g1YToz6DWhxVnt8Dy1ijR2zynRKxTyBGA8rCDXU3rs2Qc4OKvUvc2Qoe1bcFK6bnPs20TrTg==", - "dev": true, - "requires": { - "@types/cheerio": "^0.22.22", - "lodash": "^4.17.21", - "react-is": "^16.12.0" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - } - } - }, - "err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "error-stack-parser": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", - "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", - "dev": true, - "requires": { - "stackframe": "^1.1.1" - } - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.45", - "resolved": "", - "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, - "eslint": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz", - "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==", - "dev": true, - "requires": { - "@eslint/eslintrc": "^1.1.0", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", - "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.2", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" - }, - "dependencies": { - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - } - } - }, - "eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^5.0.0" - } - }, - "eslint-plugin-jsdoc": { - "version": "37.9.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-37.9.1.tgz", - "integrity": "sha512-ynIsYL+rOtIKWOttAYWCgOJawPwYKexcX3cuoYHwifvz4+uY+MZ2un5nMHBULigdSITnQ5/ZSHpO/O1nwv/uJA==", - "dev": true, - "requires": { - "@es-joy/jsdoccomment": "~0.19.0", - "comment-parser": "1.3.0", - "debug": "^4.3.3", - "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "regextras": "^0.8.0", - "semver": "^7.3.5", - "spdx-expression-parse": "^3.0.1" - }, - "dependencies": { - "comment-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.0.tgz", - "integrity": "sha512-hRpmWIKgzd81vn0ydoWoyPoALEOnF4wt8yKD35Ib1D6XC2siLiYaiqfGkYrunuKdsXGwpBpHU3+9r+RVw2NZfA==", - "dev": true - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz", - "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.3", - "aria-query": "^4.2.2", - "array-includes": "^3.1.4", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.3.5", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.7", - "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.2.1", - "language-tags": "^1.0.5", - "minimatch": "^3.0.4" - }, - "dependencies": { - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - } - } - }, - "eslint-plugin-markdown": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-2.2.1.tgz", - "integrity": "sha512-FgWp4iyYvTFxPwfbxofTvXxgzPsDuSKHQy2S+a8Ve6savbujey+lgrFFbXQA0HPygISpRYWYBjooPzhYSF81iA==", - "dev": true, - "requires": { - "mdast-util-from-markdown": "^0.8.5" - } - }, - "eslint-plugin-prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-react": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz", - "integrity": "sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-react-hooks": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, - "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", - "dev": true, - "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.3.0" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "event-stream": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" - } - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "execall": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", - "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", - "dev": true, - "requires": { - "clone-regexp": "^2.1.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - } - }, - "expect-puppeteer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", - "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", - "dev": true - }, - "express": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz", - "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.9.6", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "qs": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", - "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fancy-log": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", - "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "time-stamp": "^1.0.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "dependencies": { - "is-glob": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true - }, - "filenamify": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", - "dev": true, - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.1", - "trim-repeated": "^1.0.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", - "dev": true - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-file-up": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", - "integrity": "sha1-z2gJG8+fMApA2kEbN9pczlovvqA=", - "dev": true, - "requires": { - "fs-exists-sync": "^0.1.0", - "resolve-dir": "^0.1.0" - }, - "dependencies": { - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true, - "requires": { - "os-homedir": "^1.0.1" - } - }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - } - }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - } - }, - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true, - "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" - } - } - } - }, - "find-parent-dir": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", - "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", - "dev": true - }, - "find-pkg": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", - "integrity": "sha1-G9wiwG42NlUy4qJIBGhUuXiNpVc=", - "dev": true, - "requires": { - "find-file-up": "^0.1.2" - } - }, - "find-process": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz", - "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "commander": "^5.1.0", - "debug": "^4.1.1" - }, - "dependencies": { - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "findup": { - "version": "0.1.5", - "resolved": "", - "integrity": "sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=", - "dev": true, - "requires": { - "colors": "~0.6.0-1", - "commander": "~2.1.0" - }, - "dependencies": { - "commander": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", - "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", - "dev": true - } - } - }, - "findup-sync": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "fined": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - } - }, - "first-chunk-stream": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "flagged-respawn": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", - "dev": true - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true - }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "fork-stream": { - "version": "0.0.4", - "resolved": "", - "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=", - "dev": true - }, - "form-data": { - "version": "2.3.2", - "resolved": "", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true - }, - "fraction.js": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.3.tgz", - "integrity": "sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from": { - "version": "0.1.7", - "resolved": "", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-extra": { - "version": "0.26.7", - "resolved": "", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - } - }, - "fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "fstream": { - "version": "1.0.12", - "resolved": "", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "functions-have-names": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz", - "integrity": "sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-pkg-repo": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", - "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", - "dev": true, - "requires": { - "@hutson/parse-repository-url": "^3.0.0", - "hosted-git-info": "^4.0.0", - "through2": "^2.0.0", - "yargs": "^16.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" - } - }, - "git-raw-commits": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.10.tgz", - "integrity": "sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ==", - "dev": true, - "requires": { - "dargs": "^7.0.0", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "requires": { - "readable-stream": "3" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", - "dev": true, - "requires": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "git-semver-tags": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", - "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", - "dev": true, - "requires": { - "meow": "^8.0.0", - "semver": "^6.0.0" - }, - "dependencies": { - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "git-up": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", - "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "parse-url": "^6.0.0" - } - }, - "git-url-parse": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz", - "integrity": "sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g==", - "dev": true, - "requires": { - "git-up": "^4.0.0" - } - }, - "gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", - "dev": true, - "requires": { - "ini": "^1.3.2" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.3", - "resolved": "", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "glob-watcher": { - "version": "5.0.3", - "resolved": "", - "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "object.defaults": "^1.1.0" - }, - "dependencies": { - "chokidar": { - "version": "2.1.8", - "resolved": "", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - } - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "8.0.2", - "resolved": "", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "globjoin": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", - "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", - "dev": true - }, - "globule": { - "version": "1.3.1", - "resolved": "", - "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.12", - "minimatch": "~3.0.2" - } - }, - "glogg": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "gulp": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dev": true, - "requires": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - } - }, - "gulp-autoprefixer": { - "version": "5.0.0", - "resolved": "", - "integrity": "sha1-gjfCeKaXdScKHK/n1vEBz81YVUQ=", - "dev": true, - "requires": { - "autoprefixer": "^8.0.0", - "fancy-log": "^1.3.2", - "plugin-error": "^1.0.1", - "postcss": "^6.0.1", - "through2": "^2.0.0", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "gulp-checktextdomain": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha512-di5ptVL+KiiLq20K7sotl8Y4V7MnaSoCaiZYcJXTOFUALUIrB15+5fssPT2WfdxgT/IdyXPednEVbaJ5/1n2+A==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "fs-extra": "^0.26.7", - "php-parser": "^2.0.6", - "plugin-error": "^1.0.1", - "ramda": "^0.20.0", - "text-table": "^0.2.0", - "through2": "^2.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "gulp-cli": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.1.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", - "yargs": "^7.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "camelcase": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "y18n": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "gulp-composer": { - "version": "0.4.5", - "resolved": "", - "integrity": "sha512-k4Wep10UwqX1mKFWGaKTIJp1isuAyE+Y3OZccUCAMPvwbppNc6pYKfer25gZI82N/GYHlLKas67dRDv1SrdDVQ==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "fancy-log": "^1.3.2", - "plugin-error": "^1.0.0", - "shelljs": "~0.2.6", - "through2": "^4.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "requires": { - "readable-stream": "3" - } - } - } - }, - "gulp-filter": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", - "dev": true, - "requires": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" - }, - "dependencies": { - "arr-diff": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" - } - }, - "arr-union": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "dev": true, - "requires": { - "kind-of": "^1.1.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "dev": true, - "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - } - } - } - }, - "gulp-git": { - "version": "2.10.0", - "resolved": "", - "integrity": "sha512-AYh0xXpKdDYS+ftCuyF9+LFXoltjtFlpfKITTCKDI0LunztpwVuHFtp31SvRSFVZikvRHTHUGMZ9Z0TnXjDIxQ==", - "dev": true, - "requires": { - "any-shell-escape": "^0.1.1", - "fancy-log": "^1.3.2", - "lodash.template": "^4.4.0", - "plugin-error": "^1.0.1", - "require-dir": "^1.0.0", - "strip-bom-stream": "^3.0.0", - "through2": "^2.0.3", - "vinyl": "^2.0.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-header": { - "version": "2.0.9", - "resolved": "", - "integrity": "sha512-LMGiBx+qH8giwrOuuZXSGvswcIUh0OiioNkUpLhNyvaC6/Ga8X6cfAeme2L5PqsbXMhL8o8b/OmVqIQdxprhcQ==", - "dev": true, - "requires": { - "concat-with-sourcemaps": "^1.1.0", - "lodash.template": "^4.5.0", - "map-stream": "0.0.7", - "through2": "^2.0.0" - }, - "dependencies": { - "lodash.template": { - "version": "4.5.0", - "resolved": "", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - } - } - }, - "gulp-if": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", - "dev": true, - "requires": { - "gulp-match": "^1.0.3", - "ternary-stream": "^2.0.1", - "through2": "^2.0.1" - } - }, - "gulp-ignore": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-hRQDJipaU5A84J7QacNGG+bnFU3QLESdjuJCRTAZ5VuKsz4LmvOrECFJeeCtqxP3RV0Pn6mawU0Q3CFZGpirSg==", - "dev": true, - "requires": { - "gulp-match": "^1.1.0", - "through2": "^3.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "dev": true, - "requires": { - "readable-stream": "2 || 3" - } - } - } - }, - "gulp-include": { - "version": "2.4.1", - "resolved": "", - "integrity": "sha512-ARF7H6CD/CCavOcvlLhs6sAY+turxI72Gwp+5X/sMNUha8eJXFloDaZ93nnSKIh0K8VR7b7PURHdXrIhFhQ9gg==", - "dev": true, - "requires": { - "ansi-colors": "^3.2.4", - "event-stream": "^4.0.1", - "glob": "^7.1.3", - "plugin-error": "^1.0.1", - "source-map": "^0.7.3", - "strip-bom": "^2.0.0", - "vinyl": "^2.2.0", - "vinyl-sourcemaps-apply": "^0.2.1" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.4", - "resolved": "", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-match": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.3" - } - }, - "gulp-notify": { - "version": "3.2.0", - "resolved": "", - "integrity": "sha512-qEocs1UVoDKKUjfsxJNMNwkRla0PbsyJwsqNNXpzYWsLQ29LhxRMY3wnTGZcc4hMHtalnvah/Dwlwb4NijH/0A==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "fancy-log": "^1.3.2", - "lodash.template": "^4.4.0", - "node-notifier": "^5.2.1", - "node.extend": "^2.0.0", - "plugin-error": "^0.1.2", - "through2": "^2.0.3" - }, - "dependencies": { - "arr-diff": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" - } - }, - "arr-union": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "dev": true, - "requires": { - "kind-of": "^1.1.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "dev": true, - "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - } - } - } - }, - "gulp-rename": { - "version": "1.4.0", - "resolved": "", - "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", - "dev": true - }, - "gulp-replace": { - "version": "0.5.4", - "resolved": "", - "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk=", - "dev": true, - "requires": { - "istextorbinary": "1.0.2", - "readable-stream": "^2.0.1", - "replacestream": "^4.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "gulp-requirejs-optimize": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-6Jc8xg2tneeNVOv1cwiUtnDxJByTA2JCuJ1MbzKCvKcASdH8Y4kjJCE8Xpw1LNnkZwN18B5+vCRf9ZmEJQB6OQ==", - "dev": true, - "requires": { - "chalk": "^2.3.2", - "fancy-log": "^1.3.2", - "lodash.defaults": "^4.0.1", - "plugin-error": "^1.0.1", - "requirejs": "^2.2.0", - "through2": "^2.0.1", - "vinyl": "^2.1.0", - "vinyl-sourcemaps-apply": "^0.2.1" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "clone": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-rtlcss": { - "version": "1.4.0", - "resolved": "", - "integrity": "sha512-66UmUSacTzdV3L0KcsdwzExEu1+dTfNlq3emUZGgHPLgUaCrsZUgZwjsgKjPwkYJUZOucLpjOxAkB37k+H80Kw==", - "dev": true, - "requires": { - "plugin-error": "^1.0.1", - "rtlcss": "^2.4.0", - "through2": "^2.0.5", - "vinyl-sourcemaps-apply": "^0.2.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, - "gulp-run": { - "version": "1.7.1", - "resolved": "", - "integrity": "sha1-4XwKy3wwtuKu7iPAREKpbAys7/o=", - "dev": true, - "requires": { - "gulp-util": "^3.0.0", - "lodash.defaults": "^4.0.1", - "lodash.template": "^4.0.2", - "vinyl": "^0.4.6" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true, - "requires": { - "clone": "^0.2.0", - "clone-stats": "^0.0.1" - } - } - } - }, - "gulp-sass": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-xIiwp9nkBLcJDpmYHbEHdoWZv+j+WtYaKD6Zil/67F3nrAaZtWYN5mDwerdo7EvcdBenSAj7Xb2hx2DqURLGdA==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "lodash": "^4.17.11", - "node-sass": "^4.8.3", - "plugin-error": "^1.0.1", - "replace-ext": "^1.0.0", - "strip-ansi": "^4.0.0", - "through2": "^2.0.0", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "replace-ext": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true - } - } - }, - "gulp-sort": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-xnYqLx8N4KP8WVohWZ0/rI26Gso=", - "dev": true, - "requires": { - "through2": "^2.0.1" - } - }, - "gulp-sourcemaps": { - "version": "2.6.5", - "resolved": "", - "integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==", - "dev": true, - "requires": { - "@gulp-sourcemaps/identity-map": "1.X", - "@gulp-sourcemaps/map-sources": "1.X", - "acorn": "5.X", - "convert-source-map": "1.X", - "css": "2.X", - "debug-fabulous": "1.X", - "detect-newline": "2.X", - "graceful-fs": "4.X", - "source-map": "~0.6.0", - "strip-bom-string": "1.X", - "through2": "2.X" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "gulp-uglify": { - "version": "1.5.4", - "resolved": "", - "integrity": "sha1-UkeI2HZm0J+dDCH7IXf5ADmmWMk=", - "dev": true, - "requires": { - "deap": "^1.0.0", - "fancy-log": "^1.0.0", - "gulp-util": "^3.0.0", - "isobject": "^2.0.0", - "through2": "^2.0.0", - "uglify-js": "2.6.4", - "uglify-save-license": "^0.4.1", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "gulp-vinyl-zip": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-7tKXptewHdKnOV0HGIyB/5+dvfmwmHq+hnolAQ64zz/pPomUXJcFPeYCkatRmOztkfZOn+14zoIFS2G39PkzIg==", - "dev": true, - "requires": { - "queue": "^4.2.1", - "through": "^2.3.8", - "through2": "^2.0.3", - "vinyl": "^2.0.2", - "vinyl-fs": "^3.0.3", - "yauzl": "^2.2.1", - "yazl": "^2.2.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-wp-pot": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-RaS7MiT8w9HdfmH8c6l7QKeeZeWDMDoyWUb//vFXGH05XY1t2w4iv22k0AIWyZ9w2ceX6W2ia3jC5hH+5wn2rA==", - "dev": true, - "requires": { - "plugin-error": "^1.0.1", - "through2": "^3.0.1", - "vinyl": "^2.2.0", - "wp-pot": "^1.7.1" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "dev": true, - "requires": { - "readable-stream": "2 || 3" - } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true, - "requires": { - "glogg": "^1.0.0" - } - }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "requires": { - "duplexer": "^0.1.2" - } - }, - "handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-js": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.3.tgz", - "integrity": "sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", - "dev": true - }, - "html-element-map": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", - "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", - "dev": true, - "requires": { - "array.prototype.filter": "^1.0.0", - "call-bind": "^1.0.2" - } - }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, - "html-entities": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "html-tags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", - "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", - "dev": true - }, - "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - } - } - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - } - } - }, - "http-parser-js": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.5.tgz", - "integrity": "sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==", - "dev": true - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "http-proxy-middleware": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz", - "integrity": "sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==", - "dev": true, - "requires": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "immutable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", - "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", - "dev": true - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.7", - "resolved": "", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true - }, - "init-package-json": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.5.tgz", - "integrity": "sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA==", - "dev": true, - "requires": { - "npm-package-arg": "^8.1.5", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "^4.1.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^3.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "read-package-json": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-4.1.1.tgz", - "integrity": "sha512-P82sbZJ3ldDrWCOSKxJT0r/CXMWR0OR3KRh55SgKo3p91GSIEEC32v3lSHAvO/UcH3/IoL7uqhOFBduAnwdldw==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^3.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "inquirer": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", - "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.2.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true - }, - "irregular-plurals": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", - "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", - "dev": true - }, - "is": { - "version": "3.3.0", - "resolved": "", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true - }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", - "dev": true - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true - }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true - }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", - "dev": true - }, - "is-negated-glob": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - } - }, - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-promise": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-regexp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", - "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", - "dev": true - }, - "is-relative": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true - }, - "is-ssh": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz", - "integrity": "sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ==", - "dev": true, - "requires": { - "protocols": "^1.1.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-svg": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "dev": true, - "requires": { - "text-extensions": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-word-character": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", - "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "istextorbinary": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", - "dev": true, - "requires": { - "binaryextensions": "~1.0.0", - "textextensions": "~1.0.0" - } - }, - "jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - } - }, - "jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dev": true, - "requires": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-dev-server": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-6.0.3.tgz", - "integrity": "sha512-joKPQQWSaBMsNNdCWvwCQvhD6ox4IH+5H5pecbRRSxiRi2BfVCGGOWQ4/MGwV1NJ9z9XEq1qy5JLYTJlv9RVzA==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "cwd": "^0.10.0", - "find-process": "^1.4.7", - "prompts": "^2.4.2", - "spawnd": "^6.0.2", - "tree-kill": "^1.2.2", - "wait-on": "^6.0.0" - } - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - }, - "dependencies": { - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - } - } - }, - "jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - } - }, - "jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - }, - "jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "dependencies": { - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - } - }, - "jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dev": true, - "requires": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true - }, - "jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - } - }, - "jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - } - } - }, - "jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "requires": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "dependencies": { - "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jmespath": { - "version": "0.15.0", - "resolved": "", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", - "dev": true - }, - "joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, - "js-base64": { - "version": "2.5.2", - "resolved": "", - "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jsdoc-type-pratt-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.3.tgz", - "integrity": "sha512-QPyxq62Q8veBSDtDrWmqaEPjSCeknUV9dH/OAGt3q9an8qC8UQDqitQiw1NvoMskIESpoRZ6qzt4H3rlK0xo8A==", - "dev": true - }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json2php": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.4.tgz", - "integrity": "sha1-a9haHdpqXdfpECK7JEA8wbfC7jQ=", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", - "dev": true - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jsx-ast-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", - "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", - "dev": true, - "requires": { - "array-includes": "^3.1.3", - "object.assign": "^4.1.2" - } - }, - "just-debounce": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "klaw": { - "version": "1.3.1", - "resolved": "", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", - "dev": true - }, - "known-css-properties": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.24.0.tgz", - "integrity": "sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==", - "dev": true - }, - "language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true - }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", - "dev": true, - "requires": { - "language-subtag-registry": "~0.3.2" - } - }, - "last-run": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true - }, - "lazystream": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "dev": true, - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "lead": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", - "dev": true, - "requires": { - "flush-write-stream": "^1.0.2" - } - }, - "lerna": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz", - "integrity": "sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg==", - "dev": true, - "requires": { - "@lerna/add": "4.0.0", - "@lerna/bootstrap": "4.0.0", - "@lerna/changed": "4.0.0", - "@lerna/clean": "4.0.0", - "@lerna/cli": "4.0.0", - "@lerna/create": "4.0.0", - "@lerna/diff": "4.0.0", - "@lerna/exec": "4.0.0", - "@lerna/import": "4.0.0", - "@lerna/info": "4.0.0", - "@lerna/init": "4.0.0", - "@lerna/link": "4.0.0", - "@lerna/list": "4.0.0", - "@lerna/publish": "4.0.0", - "@lerna/run": "4.0.0", - "@lerna/version": "4.0.0", - "import-local": "^3.0.2", - "npmlog": "^4.1.2" - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "libnpmaccess": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz", - "integrity": "sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ==", - "dev": true, - "requires": { - "aproba": "^2.0.0", - "minipass": "^3.1.1", - "npm-package-arg": "^8.1.2", - "npm-registry-fetch": "^11.0.0" - }, - "dependencies": { - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "npm-registry-fetch": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz", - "integrity": "sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA==", - "dev": true, - "requires": { - "make-fetch-happen": "^9.0.1", - "minipass": "^3.1.3", - "minipass-fetch": "^1.3.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.0.0", - "npm-package-arg": "^8.0.0" - } - }, - "socks-proxy-agent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", - "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "libnpmpublish": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.2.tgz", - "integrity": "sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw==", - "dev": true, - "requires": { - "normalize-package-data": "^3.0.2", - "npm-package-arg": "^8.1.2", - "npm-registry-fetch": "^11.0.0", - "semver": "^7.1.3", - "ssri": "^8.0.1" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "npm-registry-fetch": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz", - "integrity": "sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA==", - "dev": true, - "requires": { - "make-fetch-happen": "^9.0.1", - "minipass": "^3.1.3", - "minipass-fetch": "^1.3.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.0.0", - "npm-package-arg": "^8.0.0" - } - }, - "socks-proxy-agent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", - "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "lifterlms-lib-tasks": { - "version": "3.7.0", - "resolved": "", - "integrity": "sha512-XPJ8SgJLxRmAsyibSp9w1j8BMJN/Bwr/ulv+OtEFeUVz17Vnm1vfmjX/T/rapmNlmLDt9a6duAt3UMxmmCUkGQ==", - "dev": true, - "requires": { - "ansi-colors": "^1.1.0", - "aws-sdk": "^2.481.0", - "fancy-log": "^1.3.3", - "globby": "^8.0.2", - "gulp": "^4.0.2", - "gulp-autoprefixer": "^5.0.0", - "gulp-checktextdomain": "^2.2.0", - "gulp-cli": "^2.2.0", - "gulp-composer": "^0.4.5", - "gulp-filter": "^5.1.0", - "gulp-git": "^2.9.0", - "gulp-if": "^2.0.2", - "gulp-ignore": "^3.0.0", - "gulp-include": "^2.4.1", - "gulp-rename": "^1.4.0", - "gulp-replace": "^0.6.1", - "gulp-rtlcss": "^1.4.0", - "gulp-run": "^1.7.1", - "gulp-sass": "^4.0.2", - "gulp-sort": "^2.0.0", - "gulp-sourcemaps": "^2.6.5", - "gulp-uglify": "^3.0.2", - "gulp-vinyl-zip": "^2.1.2", - "gulp-wp-pot": "^2.3.5", - "inquirer": "^5.2.0", - "merge": "^1.2.1", - "node-svn-ultimate": "^1.2.0", - "node-version-compare": "^1.0.2", - "normalize.css": "^8.0.1", - "pump": "^1.0.3", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^5.7.0", - "showdown": "^1.9.0", - "yargs": "^12.0.5" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "aws4": { - "version": "1.9.1", - "resolved": "", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "commander": { - "version": "2.20.3", - "resolved": "", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "event-stream": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "fancy-log": { - "version": "1.3.3", - "resolved": "", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "glob": { - "version": "7.1.6", - "resolved": "", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-watcher": { - "version": "5.0.3", - "resolved": "", - "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "object.defaults": "^1.1.0" - } - }, - "gulp": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dev": true, - "requires": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - } - }, - "gulp-include": { - "version": "2.4.1", - "resolved": "", - "integrity": "sha512-ARF7H6CD/CCavOcvlLhs6sAY+turxI72Gwp+5X/sMNUha8eJXFloDaZ93nnSKIh0K8VR7b7PURHdXrIhFhQ9gg==", - "dev": true, - "requires": { - "ansi-colors": "^3.2.4", - "event-stream": "^4.0.1", - "glob": "^7.1.3", - "plugin-error": "^1.0.1", - "source-map": "^0.7.3", - "strip-bom": "^2.0.0", - "vinyl": "^2.2.0", - "vinyl-sourcemaps-apply": "^0.2.1" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.4", - "resolved": "", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - } - } - }, - "gulp-replace": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha1-Eb+Mj85TPjPi9qjy9DC5VboL4GY=", - "dev": true, - "requires": { - "istextorbinary": "1.0.2", - "readable-stream": "^2.0.1", - "replacestream": "^4.0.0" - } - }, - "gulp-sourcemaps": { - "version": "2.6.5", - "resolved": "", - "integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==", - "dev": true, - "requires": { - "@gulp-sourcemaps/identity-map": "1.X", - "@gulp-sourcemaps/map-sources": "1.X", - "acorn": "5.X", - "convert-source-map": "1.X", - "css": "2.X", - "debug-fabulous": "1.X", - "detect-newline": "2.X", - "graceful-fs": "4.X", - "source-map": "~0.6.0", - "strip-bom-string": "1.X", - "through2": "2.X" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "gulp-uglify": { - "version": "3.0.2", - "resolved": "", - "integrity": "sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg==", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "extend-shallow": "^3.0.2", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "isobject": "^3.0.1", - "make-error-cause": "^1.1.1", - "safe-buffer": "^5.1.2", - "through2": "^2.0.0", - "uglify-js": "^3.0.5", - "vinyl-sourcemaps-apply": "^0.2.0" - } - }, - "har-validator": { - "version": "5.1.3", - "resolved": "", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "map-stream": { - "version": "0.0.7", - "resolved": "", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true - }, - "mem": { - "version": "4.3.0", - "resolved": "", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.2", - "resolved": "", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "rxjs": { - "version": "5.5.12", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", - "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", - "dev": true, - "requires": { - "symbol-observable": "1.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "stream-combiner": { - "version": "0.2.2", - "resolved": "", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "through": "~2.3.4" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "uglify-js": { - "version": "3.8.0", - "resolved": "", - "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", - "dev": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "undertaker": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "yargs": { - "version": "12.0.5", - "resolved": "", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "liftoff": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", - "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, - "dependencies": { - "findup-sync": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - } - } - }, - "lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "dev": true - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dev": true, - "requires": { - "uc.micro": "^1.0.1" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true - }, - "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash._root": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.differencewith": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz", - "integrity": "sha1-uvr7yRi1UVTheRdqALsK76rIVLc=", - "dev": true - }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true, - "requires": { - "lodash._root": "^3.0.0" - } - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true - }, - "lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, - "lodash.template": { - "version": "3.6.2", - "resolved": "", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" - } - }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } - }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "longest": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "longest-streak": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "lru-queue": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "dev": true, - "requires": { - "es5-ext": "~0.10.2" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "make-error-cause": { - "version": "1.2.2", - "resolved": "", - "integrity": "sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0=", - "dev": true, - "requires": { - "make-error": "^1.2.0" - } - }, - "make-fetch-happen": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz", - "integrity": "sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==", - "dev": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.0.5", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^5.0.0", - "ssri": "^8.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "map-age-cleaner": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-stream": { - "version": "0.0.7", - "resolved": "", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true - }, - "map-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", - "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-it": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz", - "integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==", - "dev": true, - "requires": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - } - } - }, - "markdownlint": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.23.1.tgz", - "integrity": "sha512-iOEwhDfNmq2IJlaA8mzEkHYUi/Hwoa6Ss+HO5jkwUR6wQ4quFr0WzSx+Z9rsWZKUaPbyirIdL1zGmJRkWawr4Q==", - "dev": true, - "requires": { - "markdown-it": "12.0.4" - } - }, - "markdownlint-cli": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.27.1.tgz", - "integrity": "sha512-p1VV6aSbGrDlpUWzHizAnSNEQAweVR3qUI/AIUubxW7BGPXziSXkIED+uRtSohUlRS/jmqp3Wi4es5j6fIrdeQ==", - "dev": true, - "requires": { - "commander": "~7.1.0", - "deep-extend": "~0.6.0", - "get-stdin": "~8.0.0", - "glob": "~7.1.6", - "ignore": "~5.1.8", - "js-yaml": "^4.0.0", - "jsonc-parser": "~3.0.0", - "lodash.differencewith": "~4.5.0", - "lodash.flatten": "~4.4.0", - "markdownlint": "~0.23.1", - "markdownlint-rule-helpers": "~0.14.0", - "minimatch": "~3.0.4", - "minimist": "~1.2.5", - "rc": "~1.2.8" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "commander": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz", - "integrity": "sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==", - "dev": true - }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, - "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - } - } - }, - "markdownlint-rule-helpers": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.14.0.tgz", - "integrity": "sha512-vRTPqSU4JK8vVXmjICHSBhwXUvbfh/VJo+j7hvxqe15tLJyomv3FLgFdFgb8kpj0Fe8SsJa/TZUAXv7/sN+N7A==", - "dev": true - }, - "matchdep": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - } - }, - "matched": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha512-mD08ireECeLL/CCgum8EeLx/SZiAmhbbt4FPlCZ4GG2xKBJ/yB8qn0uvuvouQzCORknElll2jSNVdtCWNQdR2g==", - "dev": true, - "requires": { - "glob": "^7.1.3", - "picomatch": "^2.0.5" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "mathml-tag-names": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", - "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", - "dev": true - }, - "mdast-util-from-markdown": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", - "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", - "dev": true, - "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-to-string": "^2.0.0", - "micromark": "~2.11.0", - "parse-entities": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "dependencies": { - "mdast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", - "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", - "dev": true - }, - "parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.2" - } - } - } - }, - "mdast-util-inject": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", - "integrity": "sha1-2wa4tYW+lZotzS+H9HK6m3VvNnU=", - "dev": true, - "requires": { - "mdast-util-to-string": "^1.0.0" - } - }, - "mdast-util-to-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", - "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", - "dev": true - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "dev": true, - "requires": { - "fs-monkey": "1.0.3" - } - }, - "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==", - "dev": true - }, - "memoizee": { - "version": "0.4.12", - "resolved": "", - "integrity": "sha512-sprBu6nwxBWBvBOh5v2jcsGqiGLlL2xr2dLub3vR8dnE8YB17omwtm/0NSHl8jjNbcsJd5GMWJAnTSVe/O0Wfg==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.30", - "es6-weak-map": "^2.0.2", - "event-emitter": "^0.3.5", - "is-promise": "^2.1", - "lru-queue": "0.1", - "next-tick": "1", - "timers-ext": "^0.1.2" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "merge": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-deep": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", - "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "clone-deep": "^0.2.4", - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromark": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", - "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", - "dev": true, - "requires": { - "debug": "^4.0.0", - "parse-entities": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", - "dev": true - }, - "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "dev": true, - "requires": { - "mime-db": "1.50.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "min-indent": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=", - "dev": true - }, - "mini-css-extract-plugin": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz", - "integrity": "sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw==", - "dev": true, - "requires": { - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, - "minipass": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", - "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "dev": true, - "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "requires": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", - "dev": true, - "requires": { - "for-in": "^0.1.3", - "is-extendable": "^0.1.1" - }, - "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "mkdirp-infer-owner": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz", - "integrity": "sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "infer-owner": "^1.0.4", - "mkdirp": "^1.0.3" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - } - } - }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true - }, - "moo": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", - "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", - "dev": true - }, - "mrmime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", - "integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "multimatch": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "multipipe": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "dev": true, - "requires": { - "duplexer2": "0.0.2" - } - }, - "mute-stdout": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nan": { - "version": "2.14.0", - "resolved": "", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true - }, - "nanoid": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.0.tgz", - "integrity": "sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg==", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nearley": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", - "dev": true, - "requires": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - }, - "dependencies": { - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } - }, - "node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", - "dev": true - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - } - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.4.0", - "resolved": "", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.0", - "resolved": "", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "node-sass": { - "version": "4.14.1", - "resolved": "", - "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.15", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "2.2.5", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "node-svn-ultimate": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha512-WqNyOq3hW/A2Hi/MAMFu3PhnCzGdTnGaR6Cfr65OL7Qa8pLvlMbJ21t7Vko3CYngnI6Zn1JmKwtA3SlEcFC61A==", - "dev": true, - "requires": { - "fs-extra": "^1.0.0", - "semver": "^5.3.0", - "uuid": "^3.0.0", - "xml2js": "^0.4.17" - }, - "dependencies": { - "fs-extra": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "node-version-compare": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-OVcHSPS3nROlBQXcALptOR0j2lOabC9wE2S+y+Fvr7nSDqoO/LCNzRdVGVovipeHUXs5jAqq7GelWE1X4J1sEw==", - "dev": true - }, - "node.extend": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", - "dev": true, - "requires": { - "has": "^1.0.3", - "is": "^3.2.1" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-selector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", - "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", - "dev": true - }, - "normalize-url": { - "version": "3.3.0", - "resolved": "", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "normalize.css": { - "version": "8.0.1", - "resolved": "", - "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", - "dev": true - }, - "now-and-later": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", - "dev": true, - "requires": { - "once": "^1.3.2" - } - }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-install-checks": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", - "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", - "dev": true, - "requires": { - "semver": "^7.1.1" - } - }, - "npm-lifecycle": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz", - "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", - "dev": true, - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.15", - "node-gyp": "^5.0.2", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" - }, - "dependencies": { - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "node-gyp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz", - "integrity": "sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw==", - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.1.2", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^5.7.1", - "tar": "^4.4.12", - "which": "^1.3.1" - } - }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "dev": true, - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true - }, - "npm-package-arg": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", - "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "semver": "^7.3.4", - "validate-npm-package-name": "^3.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "npm-package-json-lint": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-5.4.2.tgz", - "integrity": "sha512-DH1MSvYvm+cuQFXcPehIIu/WiYzMYs7BOxlhOOFHaH2SNrA+P2uDtTEe5LOG90Ci7PTwgF/dCmSKM2HWTgWXNA==", - "dev": true, - "requires": { - "ajv": "^6.12.6", - "ajv-errors": "^1.0.1", - "chalk": "^4.1.2", - "cosmiconfig": "^7.0.1", - "debug": "^4.3.2", - "globby": "^11.0.4", - "ignore": "^5.1.9", - "is-plain-obj": "^3.0.0", - "jsonc-parser": "^3.0.0", - "log-symbols": "^4.1.0", - "meow": "^6.1.1", - "plur": "^4.0.0", - "semver": "^7.3.5", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", - "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "^4.0.2", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "npm-packlist": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz", - "integrity": "sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==", - "dev": true, - "requires": { - "glob": "^7.1.6", - "ignore-walk": "^3.0.3", - "npm-bundled": "^1.1.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-pick-manifest": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz", - "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==", - "dev": true, - "requires": { - "npm-install-checks": "^4.0.0", - "npm-normalize-package-bin": "^1.0.1", - "npm-package-arg": "^8.1.2", - "semver": "^7.3.4" - } - }, - "npm-registry-fetch": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz", - "integrity": "sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA==", - "dev": true, - "requires": { - "@npmcli/ci-detect": "^1.0.0", - "lru-cache": "^6.0.0", - "make-fetch-happen": "^8.0.9", - "minipass": "^3.1.3", - "minipass-fetch": "^1.3.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.0.0", - "npm-package-arg": "^8.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-filter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", - "integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=", - "dev": true - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.entries": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", - "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "object.hasown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", - "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.reduce": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "dependencies": { - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - } - } - }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "p-map-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz", - "integrity": "sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q==", - "dev": true - }, - "p-pipe": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz", - "integrity": "sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw==", - "dev": true - }, - "p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - } - }, - "p-reduce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", - "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", - "dev": true - }, - "p-retry": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", - "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", - "dev": true, - "requires": { - "@types/retry": "^0.12.0", - "retry": "^0.13.1" - }, - "dependencies": { - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true - } - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "p-waterfall": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz", - "integrity": "sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw==", - "dev": true, - "requires": { - "p-reduce": "^2.0.0" - } - }, - "pacote": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz", - "integrity": "sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg==", - "dev": true, - "requires": { - "@npmcli/git": "^2.1.0", - "@npmcli/installed-package-contents": "^1.0.6", - "@npmcli/promise-spawn": "^1.2.0", - "@npmcli/run-script": "^1.8.2", - "cacache": "^15.0.5", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "infer-owner": "^1.0.4", - "minipass": "^3.1.3", - "mkdirp": "^1.0.3", - "npm-package-arg": "^8.0.1", - "npm-packlist": "^2.1.4", - "npm-pick-manifest": "^6.0.0", - "npm-registry-fetch": "^11.0.0", - "promise-retry": "^2.0.1", - "read-package-json-fast": "^2.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.1.0" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "npm-registry-fetch": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz", - "integrity": "sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA==", - "dev": true, - "requires": { - "make-fetch-happen": "^9.0.1", - "minipass": "^3.1.3", - "minipass-fetch": "^1.3.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.0.0", - "npm-package-arg": "^8.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "socks-proxy-agent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", - "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-entities": { - "version": "1.2.2", - "resolved": "", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-node-version": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse-path": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz", - "integrity": "sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "protocols": "^1.4.0", - "qs": "^6.9.4", - "query-string": "^6.13.8" - }, - "dependencies": { - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } - } - }, - "parse-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.0.tgz", - "integrity": "sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw==", - "dev": true, - "requires": { - "is-ssh": "^1.3.0", - "normalize-url": "^6.1.0", - "parse-path": "^4.0.0", - "protocols": "^1.4.0" - }, - "dependencies": { - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - } - } - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dev": true, - "requires": { - "parse5": "^6.0.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-root": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-sort": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-ywF11Oy/paGP5nTMbXIL/hXguAU=", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "php-parser": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha1-ZzhPClkz2770C+qwqzHQuMWC/4g=", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "plugin-error": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - } - }, - "plur": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", - "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", - "dev": true, - "requires": { - "irregular-plurals": "^3.2.0" - } - }, - "portfinder": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", - "dev": true, - "requires": { - "async": "^2.6.2", - "debug": "^3.1.1", - "mkdirp": "^0.5.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", - "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", - "dev": true, - "requires": { - "nanoid": "^3.2.0", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-calc": { - "version": "7.0.2", - "resolved": "", - "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", - "dev": true, - "requires": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", - "dev": true - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0" - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-resolve-nested-selector": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", - "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", - "dev": true - }, - "postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true - }, - "postcss-scss": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.3.tgz", - "integrity": "sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA==", - "dev": true - }, - "postcss-selector-parser": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", - "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "4.0.2", - "resolved": "", - "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", - "dev": true, - "requires": { - "is-svg": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "npm:wp-prettier@2.2.1-beta-1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.2.1-beta-1.tgz", - "integrity": "sha512-+JHkqs9LC/JPp51yy1hzs3lQ7qeuWCwOcSzpQNeeY/G7oSpnF61vxt7hRh87zNRTr6ob2ndy0W8rVzhgrcA+Gw==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "promzard": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", - "dev": true, - "requires": { - "read": "1" - } - }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - } - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true - }, - "protocols": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", - "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "dependencies": { - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - } - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", - "dev": true - }, - "pump": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "puppeteer-core": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-11.0.0.tgz", - "integrity": "sha512-hfQ39KNP0qKplQ86iaCNXHH9zpWlV01UFdggt2qffgWeCBF9KMavwP/k/iK/JidPPWfOnKZhDLSHZVSUr73DtA==", - "dev": true, - "requires": { - "debug": "4.3.2", - "devtools-protocol": "0.0.901419", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.0", - "node-fetch": "2.6.5", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.2.3" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "dev": true - } - } - }, - "q": { - "version": "1.5.1", - "resolved": "", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", - "dev": true, - "requires": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "queue": { - "version": "4.5.1", - "resolved": "", - "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true - }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "requires": { - "performance-now": "^2.1.0" - } - }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", - "dev": true - }, - "ramda": { - "version": "0.20.1", - "resolved": "", - "integrity": "sha1-yB6PteeLXv8iaTSe4ENBN7THRlE=", - "dev": true - }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - } - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", - "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", - "dev": true, - "requires": { - "bytes": "3.1.1", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", - "dev": true - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "react-refresh": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", - "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", - "dev": true - }, - "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-cmd-shim": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz", - "integrity": "sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw==", - "dev": true - }, - "read-package-json": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.1.tgz", - "integrity": "sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^3.0.0", - "npm-normalize-package-bin": "^1.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "read-package-json-fast": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", - "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", - "dev": true, - "requires": { - "json-parse-even-better-errors": "^2.3.0", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "read-package-tree": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", - "dev": true, - "requires": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - }, - "dependencies": { - "read-package-json": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "dev": true, - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", - "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", - "dev": true, - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "regenerator-transform": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", - "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", - "integrity": "sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "regexpu-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", - "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", - "dev": true, - "requires": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.0.1", - "regjsgen": "^0.6.0", - "regjsparser": "^0.8.2", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" - } - }, - "regextras": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", - "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", - "dev": true - }, - "regjsgen": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", - "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", - "dev": true - }, - "regjsparser": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", - "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "remove-bom-buffer": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - } - }, - "remove-bom-stream": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", - "dev": true, - "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "replace-homedir": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - } - }, - "replace-in-file": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz", - "integrity": "sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "glob": "^7.2.0", - "yargs": "^17.2.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", - "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "replacestream": { - "version": "4.0.3", - "resolved": "", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "request": { - "version": "2.88.2", - "resolved": "", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "require-dir": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-LY85DTSu+heYgDqq/mK+7zFHWkttVNRXC9NKcKGyuGLdlsfbjEPrIEYdCVrx6hqnJb+xSu3Lzaoo8VnmOhhjNA==", - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true - }, - "requirejs": { - "version": "2.3.5", - "resolved": "", - "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-bin": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/resolve-bin/-/resolve-bin-0.4.3.tgz", - "integrity": "sha512-9u8TMpc+SEHXxQXblXHz5yRvRZERkCZimFN9oz85QI3uhkh7nqfjm6OGTLg+8vucpXGcY4jLK6WkylPmt7GSvw==", - "dev": true, - "requires": { - "find-parent-dir": "~0.3.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-options": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", - "dev": true, - "requires": { - "value-or-function": "^3.0.0" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", - "dev": true, - "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" - } - }, - "rtlcss": { - "version": "2.5.0", - "resolved": "", - "integrity": "sha512-NCVdF45w70/3CQeqVvQ84bu2HN8agNn+CDjw+RxXaiWb7mPOmEvltdd1z4qzm9kin4Jnu9ShFBIx28yvWerZ2g==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "findup": "^0.1.5", - "mkdirp": "^0.5.1", - "postcss": "^6.0.23", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "resolved": "", - "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "rxjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", - "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", - "dev": true, - "requires": { - "tslib": "~2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sass": { - "version": "1.49.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz", - "integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "sass-graph": { - "version": "2.2.5", - "resolved": "", - "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^13.3.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - } - } - }, - "sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dev": true, - "requires": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - } - }, - "sax": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", - "dev": true - }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz", - "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==", - "dev": true, - "requires": { - "node-forge": "^1.2.0" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", - "dev": true, - "requires": { - "sver-compat": "^1.5.0" - } - }, - "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.2" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "shallow-clone": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", - "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", - "dev": true, - "requires": { - "is-extendable": "^0.1.1", - "kind-of": "^2.0.1", - "lazy-cache": "^0.2.3", - "mixin-object": "^2.0.1" - }, - "dependencies": { - "kind-of": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", - "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", - "dev": true, - "requires": { - "is-buffer": "^1.0.2" - } - }, - "lazy-cache": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", - "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", - "dev": true - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.2.6", - "resolved": "", - "integrity": "sha1-kEktcv/MgVmXa6umL7D2iE8MM3g=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "showdown": { - "version": "1.9.1", - "resolved": "", - "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", - "dev": true, - "requires": { - "yargs": "^14.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.2.2", - "resolved": "", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "14.2.2", - "resolved": "", - "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.0" - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", - "dev": true, - "requires": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "requires": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - } - } - }, - "socks": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", - "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", - "dev": true, - "requires": { - "ip": "^1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", - "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "4", - "socks": "^2.3.3" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "sort-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", - "integrity": "sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==", - "dev": true, - "requires": { - "is-plain-obj": "^2.0.0" - }, - "dependencies": { - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - } - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "source-map-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", - "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "sparkles": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true - }, - "spawnd": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-6.0.2.tgz", - "integrity": "sha512-+YJtx0dvy2wt304MrHD//tASc84zinBUYU1jacPBzrjhZUd7RsDo25krxr4HUHAQzEQFuMAs4/p+yLYU5ciZ1w==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "signal-exit": "^3.0.6", - "tree-kill": "^1.2.2" - }, - "dependencies": { - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - } - } - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", - "dev": true - }, - "spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "specificity": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", - "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "requires": { - "readable-stream": "^3.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "sshpk": { - "version": "1.14.2", - "resolved": "", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dev": true, - "requires": { - "minipass": "^3.1.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true - }, - "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "stackframe": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", - "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", - "dev": true - }, - "state-toggle": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "stream-combiner": { - "version": "0.2.2", - "resolved": "", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "through": "~2.3.4" - } - }, - "stream-exhaust": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamfilter": { - "version": "1.0.7", - "resolved": "", - "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", - "dev": true - }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string.prototype.matchall": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", - "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - } - }, - "string.prototype.trim": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz", - "integrity": "sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-bom-buf": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=", - "dev": true, - "requires": { - "is-utf8": "^0.2.1" - } - }, - "strip-bom-stream": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-lWvMXYRDD2klapDtgjdlzYWOFZw=", - "dev": true, - "requires": { - "first-chunk-stream": "^2.0.0", - "strip-bom-buf": "^1.0.0" - } - }, - "strip-bom-string": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "strong-log-transformer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", - "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - } - }, - "style-search": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", - "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", - "dev": true - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.13.0", - "resolved": "", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "node-releases": { - "version": "1.1.59", - "resolved": "", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==", - "dev": true - }, - "postcss": { - "version": "7.0.32", - "resolved": "", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "stylelint": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.5.0.tgz", - "integrity": "sha512-4dvQjrhAz2njLoE1OvUEZpryNWcmx2w5Lq5jlibxFv6b5W6O8/vob12M2ZzhX3Ndzs5f67F+BEYmhnQXOwfVYQ==", - "dev": true, - "requires": { - "balanced-match": "^2.0.0", - "colord": "^2.9.2", - "cosmiconfig": "^7.0.1", - "css-functions-list": "^3.0.0", - "debug": "^4.3.3", - "execall": "^2.0.0", - "fast-glob": "^3.2.11", - "fastest-levenshtein": "^1.0.12", - "file-entry-cache": "^6.0.1", - "get-stdin": "^8.0.0", - "global-modules": "^2.0.0", - "globby": "^11.1.0", - "globjoin": "^0.1.4", - "html-tags": "^3.1.0", - "ignore": "^5.2.0", - "import-lazy": "^4.0.0", - "imurmurhash": "^0.1.4", - "is-plain-object": "^5.0.0", - "known-css-properties": "^0.24.0", - "mathml-tag-names": "^2.1.3", - "meow": "^9.0.0", - "micromatch": "^4.0.4", - "normalize-path": "^3.0.0", - "normalize-selector": "^0.2.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.6", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0", - "resolve-from": "^5.0.0", - "specificity": "^0.4.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "style-search": "^0.1.0", - "supports-hyperlinks": "^2.2.0", - "svg-tags": "^1.0.0", - "table": "^6.8.0", - "v8-compile-cache": "^2.3.0", - "write-file-atomic": "^4.0.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "balanced-match": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", - "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "postcss": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", - "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", - "dev": true, - "requires": { - "nanoid": "^3.2.0", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-selector-parser": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz", - "integrity": "sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "write-file-atomic": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", - "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "stylelint-config-recommended": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz", - "integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==", - "dev": true - }, - "stylelint-config-recommended-scss": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-5.0.2.tgz", - "integrity": "sha512-b14BSZjcwW0hqbzm9b0S/ScN2+3CO3O4vcMNOw2KGf8lfVSwJ4p5TbNEXKwKl1+0FMtgRXZj6DqVUe/7nGnuBg==", - "dev": true, - "requires": { - "postcss-scss": "^4.0.2", - "stylelint-config-recommended": "^6.0.0", - "stylelint-scss": "^4.0.0" - } - }, - "stylelint-scss": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.1.0.tgz", - "integrity": "sha512-BNYTo7MMamhFOlcaAWp2dMpjg6hPyM/FFqfDIYzmYVLMmQJqc8lWRIiTqP4UX5bresj9Vo0dKC6odSh43VP2NA==", - "dev": true, - "requires": { - "lodash": "^4.17.21", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - } - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "sver-compat": { - "version": "1.5.0", - "resolved": "", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "requires": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", - "dev": true - }, - "svgo": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - } - } - }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", - "dev": true - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "table": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", - "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "tannin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", - "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", - "dev": true, - "requires": { - "@tannin/plural-forms": "^1.1.0" - } - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "tar": { - "version": "2.2.2", - "resolved": "", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true - }, - "temp-write": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz", - "integrity": "sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "is-stream": "^2.0.0", - "make-dir": "^3.0.0", - "temp-dir": "^1.0.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - } - } - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "ternary-stream": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha512-j6ei9hxSoyGlqTmoMjOm+QNvUKDOIY6bNl4Uh1lhBvl6yjPW2iLqxDUYyfDPZknQ4KdRziFl+ec99iT4l7g0cw==", - "dev": true, - "requires": { - "duplexify": "^3.5.0", - "fork-stream": "^0.0.4", - "merge-stream": "^1.0.0", - "through2": "^2.0.1" - } - }, - "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", - "dev": true, - "requires": { - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "textextensions": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", - "dev": true - }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.3", - "resolved": "", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "dev": true, - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true - }, - "timers-ext": { - "version": "0.1.5", - "resolved": "", - "integrity": "sha512-tsEStd7kmACHENhsUPaxb8Jf8/+GZZxyNFQbZD07HQOyooOa6At1rQqjffgvg7n+dxscQa9cjjMdWhJtsP2sxg==", - "dev": true, - "requires": { - "es5-ext": "~0.10.14", - "next-tick": "1" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "to-through": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", - "dev": true, - "requires": { - "through2": "^2.0.3" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true - }, - "totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "trim": { - "version": "0.0.1", - "resolved": "", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "trim-trailing-lines": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", - "dev": true - }, - "trough": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - } - }, - "tsconfig-paths": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", - "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", - "integrity": "sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true - }, - "uglify-js": { - "version": "2.6.4", - "resolved": "", - "integrity": "sha1-ZeovswWck5RpLxX+2HwrNsFrmt8=", - "dev": true, - "requires": { - "async": "~0.2.6", - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "yargs": { - "version": "3.10.0", - "resolved": "", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-save-license": { - "version": "0.4.1", - "resolved": "", - "integrity": "sha1-lXJsF8xv0XHDYX479NjYKqjEzOE=", - "dev": true - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "dev": true - }, - "umask": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", - "dev": true - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } - } - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "undertaker": { - "version": "1.2.1", - "resolved": "", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - } - }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", - "dev": true - }, - "unherit": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "dev": true, - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", - "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", - "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true, - "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" - } - }, - "unist-util-is": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "dev": true - }, - "unist-util-remove-position": { - "version": "1.1.4", - "resolved": "", - "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", - "dev": true - }, - "unist-util-visit": { - "version": "1.4.1", - "resolved": "", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "dev": true, - "requires": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" - } - }, - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.10.3", - "resolved": "", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", - "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util-promisify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", - "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "util.promisify": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - } - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "v8flags": { - "version": "3.1.1", - "resolved": "", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "dev": true, - "requires": { - "builtins": "^1.0.3" - } - }, - "value-or-function": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "vendors": { - "version": "1.0.4", - "resolved": "", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vfile-location": { - "version": "2.0.6", - "resolved": "", - "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", - "dev": true - }, - "vfile-message": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", - "dev": true, - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - }, - "vinyl": { - "version": "0.5.3", - "resolved": "", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "is-valid-glob": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", - "dev": true, - "requires": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", - "dev": true, - "requires": { - "source-map": "^0.5.1" - } - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, - "wait-on": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz", - "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==", - "dev": true, - "requires": { - "axios": "^0.25.0", - "joi": "^17.6.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^7.5.4" - }, - "dependencies": { - "rxjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.4.tgz", - "integrity": "sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } - } - }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, - "watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", - "dev": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "dependencies": { - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - } - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "webpack": { - "version": "5.69.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.0.tgz", - "integrity": "sha512-E5Fqu89Gu8fR6vejRqu26h8ld/k6/dCVbeGUcuZjc+goQHDfCPU9rER71JmdtBYGmci7Ec2aFEATQ2IVXKy2wg==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", - "webpack-sources": "^3.2.3" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - } - } - }, - "webpack-bundle-analyzer": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", - "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", - "dev": true, - "requires": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - } - } - }, - "webpack-cli": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", - "dev": true, - "requires": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.1", - "@webpack-cli/info": "^1.4.1", - "@webpack-cli/serve": "^1.6.1", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "execa": "^5.0.0", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true - }, - "rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "requires": { - "resolve": "^1.9.0" - } - } - } - }, - "webpack-dev-middleware": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", - "dev": true, - "requires": { - "colorette": "^2.0.10", - "memfs": "^3.4.1", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "webpack-dev-server": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", - "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", - "dev": true, - "requires": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.2.2", - "ansi-html-community": "^0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "default-gateway": "^6.0.3", - "del": "^6.0.0", - "express": "^4.17.1", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.0", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "portfinder": "^1.0.28", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "strip-ansi": "^7.0.0", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "del": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", - "dev": true, - "requires": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true - } - } - }, - "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "dependencies": { - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - } - } - }, - "webpack-rtl-plugin": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-lROgFkiPjapg9tcZ8FiLWeP5pJoG00018aEjLTxSrVldPD1ON+LPlhKPHjb7eE8Bc0+KL23pxcAjWDGOv9+UAw==", - "dev": true, - "requires": { - "@romainberger/css-diff": "^1.0.3", - "async": "^2.0.0", - "cssnano": "4.1.10", - "rtlcss": "2.4.0", - "webpack-sources": "1.3.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "rtlcss": { - "version": "2.4.0", - "resolved": "", - "integrity": "sha512-hdjFhZ5FCI0ABOfyXOMOhBtwPWtANLCG7rOiOcRf+yi5eDdxmDjqBruWouEnwVdzfh/TWF6NNncIEsigOCFZOA==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "findup": "^0.1.5", - "mkdirp": "^0.5.1", - "postcss": "^6.0.14", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "webpack-sources": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true - }, - "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "window-size": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true - }, - "wp-pot": { - "version": "1.8.0", - "resolved": "", - "integrity": "sha512-/mAX/emTaZ8IByXLVWHILzP6Epm22kk09S5vaUD20xpirqV6EzTF4Cn5JPivcbanczo6Bb98JR6B/mL8EHHQMA==", - "dev": true, - "requires": { - "matched": "^4.0.0", - "path-sort": "^0.1.0", - "php-parser": "^3.0.0-prerelease.9" - }, - "dependencies": { - "php-parser": { - "version": "3.0.0-prerelease.9", - "resolved": "", - "integrity": "sha512-QTVGKeiGZyRq7NpXMx15Dkiq9+B2KLGStck1Wrik+Hui+vb70rDBF+dY1RD6/IC8Wy/tUAhcKiCfKWVJUjymDA==", - "dev": true - } - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "write-json-file": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz", - "integrity": "sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==", - "dev": true, - "requires": { - "detect-indent": "^6.0.0", - "graceful-fs": "^4.1.15", - "is-plain-obj": "^2.0.0", - "make-dir": "^3.0.0", - "sort-keys": "^4.0.0", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - } - } - }, - "write-pkg": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz", - "integrity": "sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA==", - "dev": true, - "requires": { - "sort-keys": "^2.0.0", - "type-fest": "^0.4.1", - "write-json-file": "^3.2.0" - }, - "dependencies": { - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", - "dev": true - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "write-json-file": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz", - "integrity": "sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==", - "dev": true, - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.15", - "make-dir": "^2.1.0", - "pify": "^4.0.1", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.4.2" - } - } - } - }, - "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "dev": true - }, - "x-is-string": { - "version": "0.1.0", - "resolved": "", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "4.0.1", - "resolved": "", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-limit": { - "version": "2.2.2", - "resolved": "", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yazl": { - "version": "2.5.1", - "resolved": "", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index a7e0038fd2..0000000000 --- a/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "lifterlms", - "version": "5.9.0", - "description": "LifterLMS by codeBOX", - "repository": { - "type": "git", - "url": "https://github.com/gocodebox/lifterlms.git" - }, - "author": "Team LifterLMS <team@lifterlms.com>", - "license": "GPL-3.0", - "bugs": { - "url": "https://github.com/gocodebox/lifterlms/issues" - }, - "homepage": "https://lifterlms.com", - "devDependencies": { - "@lifterlms/brand": "file:packages/brand", - "@lifterlms/dev": "file:packages/dev", - "@lifterlms/llms-e2e-test-utils": "file:packages/llms-e2e-test-utils", - "@lifterlms/scripts": "file:packages/scripts", - "@wordpress/docgen": "^1.18.0", - "gulp": "^4.0.0", - "gulp-cli": "^2.2.0", - "gulp-header": "^2.0.9", - "gulp-ignore": "^3.0.0", - "gulp-include": "^2.4.1", - "gulp-notify": "^3.2.0", - "gulp-rename": "^1.2.0", - "gulp-replace": "^0.5.4", - "gulp-requirejs-optimize": "^1.2.0", - "gulp-sourcemaps": "^2.6.5", - "gulp-uglify": "^1.5.4", - "lerna": "^4.0.0", - "lifterlms-lib-tasks": "^3.7.0", - "postcss": "^8.4.6", - "yargs": "^12.0.5" - }, - "scripts": { - "build": "npm run build:scripts && npm run build:scripts:legacy && npm run build:styles && npm run build:pot && llms-dev readme", - "build:pot": "gulp pot-js && llms-dev pot", - "build:scripts": "wp-scripts build", - "build:scripts:legacy": "gulp scripts && gulp js-additional && gulp js-builder", - "build:styles": "gulp styles && gulp styles-rtl", - "dev": "llms-dev", - "lerna": "lerna", - "lint:js": "wp-scripts lint-js ./src/**/*.js", - "test": "wp-scripts test-e2e --config packages/scripts/e2e/jest.config.js", - "test:dev": "npm run test -- --puppeteer-interactive", - "pkg:docgen": "lerna run docgen", - "pkg:hoist": "lerna bootstrap --hoist", - "pkg:lint:js": "wp-scripts lint-js ./packages/llms-e2e-test-utils/**/*.js ./packages/dev", - "pkg:test": "wp-scripts test-unit-js ./packages --config packages/scripts/config/jest-unit.config.js --verbose", - "postinstall": "npm run pkg:hoist", - "start": "wp-scripts start" - } -} diff --git a/packages/README.md b/packages/README.md deleted file mode 100644 index f5b30f0229..0000000000 --- a/packages/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Packages -======== - -@todo diff --git a/packages/brand/README.md b/packages/brand/README.md deleted file mode 100644 index 0e40e6db7e..0000000000 --- a/packages/brand/README.md +++ /dev/null @@ -1,46 +0,0 @@ -LifterLMS Brand -=============== - -LifterLMS brand icons, colors, and more. - -## Installation - -Install the module - -``` -npm install --save @lifterlms/brand -``` - -## Usage - -### SCSS Colors - -Import LifterLMS brand colors and WordPress core admin colors: - -_Note: Ensure that `node_modules` is included in your SASS load path!_ - -```scss -// Import all brand files. -@import '@lifterlms/brand/sass/brand' - -// Import colors only. -@import '@lifterlms/brand/sass/colors' - -// Import typography only. -@import '@lifterlms/brand/sass/typography' - -// Use a color. -body { - background: llms-color( llms-blue ); -} - -// Use the gradient mixin. -.banner { - @include llms-gradient-bg(); -} - -// Use a font. -body { - font-family: llms-font( llms-sans ); -} -` diff --git a/packages/brand/package.json b/packages/brand/package.json deleted file mode 100644 index 7f615254c9..0000000000 --- a/packages/brand/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@lifterlms/brand", - "version": "0.0.2", - "description": "LifterLMS brand icons, colors, and more.", - "author": "Team LifterLMS <team@lifterlms.com>", - "license": "GPL-3.0-or-later", - "homepage": "https://github.com/gocodebox/lifterlms/tree/master/packages/brand", - "keywords": [ - "lifterlms", - "wordpress" - ], - "repository": { - "type": "git", - "url": "https://github.com/gocodebox/lifterlms.git", - "directory": "packages/brand" - }, - "bugs": { - "url": "https://github.com/gocodebox/lifterlms/labels/package%3A%20brand" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/brand/sass/brand.scss b/packages/brand/sass/brand.scss deleted file mode 100644 index 69708e5bf7..0000000000 --- a/packages/brand/sass/brand.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'colors'; -@import 'typography'; diff --git a/packages/brand/sass/colors.scss b/packages/brand/sass/colors.scss deleted file mode 100644 index f5c7db98d7..0000000000 --- a/packages/brand/sass/colors.scss +++ /dev/null @@ -1,100 +0,0 @@ -// Color Map. -$colors: ( - - // LifterLMS Brand Colors. - llms-blue: #466dd8, - llms-blue-alt: #2295ff, - llms-orange: #f8954f, - - // Standard colors. - white: #fff, - black: #000, - - // WP Colors. - // @link https://make.wordpress.org/core/2021/02/23/standardization-of-wp-admin-colors-in-wordpress-5-7/ - wp-gray-0: #f6f7f7, - wp-gray-2: #f0f0f1, - wp-gray-5: #dcdcde, - wp-gray-10: #c3c4c7, - wp-gray-20: #a7aaad, - wp-gray-30: #8c8f94, - wp-gray-40: #787c82, - wp-gray-50: #646970, - wp-gray-60: #50575e, - wp-gray-70: #3c434a, - wp-gray-80: #2c3338, - wp-gray-90: #1d2327, - wp-gray-100: #101517, - wp-blue-0: #f0f6fc, - wp-blue-5: #c5d9ed, - wp-blue-10: #9ec2e6, - wp-blue-20: #72aee6, - wp-blue-30: #4f94d4, - wp-blue-40: #3582c4, - wp-blue-50: #2271b1, - wp-blue-60: #135e96, - wp-blue-70: #0a4b78, - wp-blue-80: #043959, - wp-blue-90: #01263a, - wp-blue-100: #00131c, - wp-red-0: #fcf0f1, - wp-red-5: #facfd2, - wp-red-10: #ffabaf, - wp-red-20: #ff8085, - wp-red-30: #f86368, - wp-red-40: #e65054, - wp-red-50: #d63638, - wp-red-60: #b32d2e, - wp-red-70: #8a2424, - wp-red-80: #691c1c, - wp-red-90: #451313, - wp-red-100: #240a0a, - wp-yellow-0: #fcf9e8, - wp-yellow-5: #f5e6ab, - wp-yellow-10: #f2d675, - wp-yellow-20: #f0c33c, - wp-yellow-30: #dba617, - wp-yellow-40: #bd8600, - wp-yellow-50: #996800, - wp-yellow-60: #755100, - wp-yellow-70: #614200, - wp-yellow-80: #4a3200, - wp-yellow-90: #362400, - wp-yellow-100: #211600, - wp-green-0: #edfaef, - wp-green-5: #b8e6bf, - wp-green-10: #68de7c, - wp-green-20: #1ed14b, - wp-green-30: #00ba37, - wp-green-40: #00a32a, - wp-green-50: #008a20, - wp-green-60: #007017, - wp-green-70: #005c12, - wp-green-80: #00450c, - wp-green-90: #003008, - wp-green-100: #001c05 - -); - -// Simple function to retreive colors in the $colors map. -// e.g. `background-color: color( gray-50 );` -@function llms-color( $key ) { - @if map-has-key( $colors, $key ) { - @return map-get( $colors, $key ); - } - - @warn "Unknown `#{$key}` in $colors."; - @return null; -} - - -@function llms-gradient() { - @return linear-gradient( 45deg, llms-color( llms-blue-alt ), llms-color( llms-blue ) ); -} - -// Use the LifterLMS brand gradient as the background of an element. -// e.g.: `@include llms-gradient-bg()` -@mixin llms-gradient-bg() { - background: llms-color( llms-blue ); - background: llms-gradient(); -} diff --git a/packages/brand/sass/typography.scss b/packages/brand/sass/typography.scss deleted file mode 100644 index 0ec87ae597..0000000000 --- a/packages/brand/sass/typography.scss +++ /dev/null @@ -1,13 +0,0 @@ -$fonts: ( - llms-sans: 'Montserrat, sans-serif', - llms-mono: monospace, -); - -@function llms-font( $key ) { - @if map-has-key( $fonts, $key ) { - @return map-get( $fonts, $key ); - } - - @warn "Unknown `#{$key}` in $fonts."; - @return null; -} diff --git a/packages/dev/.llmsdev.yml b/packages/dev/.llmsdev.yml deleted file mode 100644 index 66965cbfef..0000000000 --- a/packages/dev/.llmsdev.yml +++ /dev/null @@ -1,2 +0,0 @@ -update-version: - skip-config: true diff --git a/packages/dev/.npmrc b/packages/dev/.npmrc deleted file mode 100644 index 86916fa50f..0000000000 --- a/packages/dev/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -package-lock=false -engine-strict=true diff --git a/packages/dev/CHANGELOG.md b/packages/dev/CHANGELOG.md deleted file mode 100644 index 1b375a2843..0000000000 --- a/packages/dev/CHANGELOG.md +++ /dev/null @@ -1,29 +0,0 @@ -@lifterlms/dev CHANGELOG -======================== - -v0.0.4 - 2022-02-15 -------------------- - -+ Added: New utility methods for generating links to the project's GitHub repository. -+ Fixed: Incorrect issue link URL generated during `changelog write` command. - - -v0.0.3 - 2021-12-23 -------------------- - -+ Bugfix: [**Breaking**] The short option `-t` for the `--title` option for the `changelog add` command has been changed to `-T`. - - -v0.0.2 - 2021-11-10 -------------------- - -+ 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. -+ Fixed an OSX issue encountered in the `changelog write` command resulting from the use of `xargs -d`. -+ Fixed an issue causing `changelog version next` to fail when passing the `--preid` flag. -+ Don't provide a default value to the `update-version` command option `--preid`. - - -v0.0.1 - 2021-11-05 -------------------- - -+ Initial release. diff --git a/packages/dev/README.md b/packages/dev/README.md deleted file mode 100644 index a57a0609e7..0000000000 --- a/packages/dev/README.md +++ /dev/null @@ -1,369 +0,0 @@ -LifterLMS Dev CLI ------------------ - -A 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... - ---- - -## CHANGELOG - -[CHANGELOG](./CHANGELOG.md) - - -## Installation - -```bash -npm install --save-dev @lifterlms/dev` -``` - - -## Setup - -This 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: - -```json -{ - "scripts": { - "dev": "llms-dev" - } -} -``` - -After adding this you may access any of the CLI's commands by running: - -```bash -npm run dev <command> [options...] -``` - - -## Available Commands and Usage - -List available commands by running: `llms-dev help`. - -Get help with a specific command by running: `llms-dev help [command]`. - -<!-- START TOKEN(Autogenerated API docs) --> - -### changelog add - -```bash -Usage: llms-dev changelog add [options] - -Create a new changelog entry. - -Options: - -s, --significance <level> The semantic version significance of the - change. Accepts: major, minor, patch. - (default: "patch") - -t, --type <type> The type of change. Accepts: added, changed, - fixed, deprecated, removed, dev, performance, - security. (default: "changed") - -c, --comment <comment> An internal-use comment to include with the - changelog entry which is not published with - the final changelog. - -l, --links <issues...> Link the changelog to one or more GitHub - issues. Can be provided multiple times to link - to multiple issues. - -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. - -e, --entry <entry> The changelog entry. - -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. (default: "trunk") - -i, --interactive Create the changelog interactively. (default: - false) - -E, --use-editor When creating a changelog interactively, will - open an editor to write the entry, This is - useful when creating multi-line entries. - -d, --dir <directory> Directory where changelog entries are stored. - (default: ".changelogs") - -h, --help display help for command - -``` - -### changelog list - -```bash -Usage: llms-dev changelog list [options] - -List existing changelog entries. - -Options: - -d, --dir <directory> Directory where changelog entries are stored. - (default: ".changelogs") - -h, --help display help for command - -``` - -### changelog validate - -```bash -Usage: llms-dev changelog validate [options] [entries...] - -Validate existing changelog entries. - -Arguments: - entries Optionally specify a list of changelog entries to - validate. If omitted will validate all existing - entries. - -Options: - -f, --format [format] Output format. Accepts: list, json, yaml. (default: - "list") - -s, --silent Skip validation output and communicate validation - status only through the exit status of the command. - -d, --dir <directory> Directory where changelog entries are stored. - (default: ".changelogs") - -h, --help display help for command - -``` - -### changelog version - -```bash -Usage: llms-dev changelog version [options] <which> - -List existing changelog entries. - -Arguments: - which Which version to retrieve. Accepts: current, next. - -Options: - -p, --preid <identifier> Identifier to be used to prefix premajor, preminor, - prepatch or prerelease version increments. - -d, --dir <directory> Directory where changelog entries are stored. - (default: ".changelogs") - -h, --help display help for command - -``` - -### changelog write - -```bash -Usage: llms-dev changelog write [options] - -Write existing changelog entries to the changelog file. - -Options: - -p, --preid <identifier> Identifier to be used to prefix premajor, preminor, - prepatch or prerelease version increments. - -F, --force <version> Use the specified version string instead of - determining the version based on changelog entry - significance. - -l, --log-file <file> The changelog file. (default: "CHANGELOG.md") - -d, --date <YYYY-MM-DD> Changelog publication date. (default: "2021-12-23") - -L, --links Add GitHub links to templates and issues in - changelog entries. (default: false) - -n, --no-links Do not add GitHub links in changelog entries. Use - this option to override the --links flag. - -D, --dry-run Output what would be written to the changelog - instead of writing it to the changelog file. - -k, --keep-entries Preserve entry files deletion after the changelog - is written. - -d, --dir <directory> Directory where changelog entries are stored. - (default: ".changelogs") - -h, --help display help for command - -``` - -### docgen - -```bash -Usage: llms-dev docgen [options] - -Generates documentation for the CLI. - -Options: - -h, --help display help for command - -``` - -### pot - -```bash -Usage: llms-dev pot [options] - -Generate i18n pot and json files using the WP-CLI. - -Options: - -d, --text-domain <text-domain> Specify the text domain. Used to generate - the filenames for generated files. (default: - "dev") - -e, --exclude <glob...> Specify files to exclude from scanning. - (default: "vendor/**, node_modules/**, - tmp/**, dist/**, docs/**, src/**, tests/**, - *.js.map") - -ee, --extra-exclude <glob...> Additional files to add to the --exclude - option. - -d, --dir <directory> Output directory where generated files will - be stored. (default: "i18n") - -t, --translator <translator> Customize the Last Translator header. - (default: "Team LifterLMS - <team@lifterlms.com>") - -b, --bugs <url> Customize the bug report location header. - (default: - "https://lifterlms.com/my-account/my-tickets") - -h, --help display help for command - -``` - -### readme - -```bash -Usage: llms-dev readme [options] - -Create a readme.txt file suitable for the WordPress.org plugin repository. - -Options: - -o, --output-file <filename> Specify the output readme file name. - (default: "readme.txt") - -i, --input-file <filename> Specify the input changelog file name. - (default: "CHANGELOG.md") - -d, --dir <directory> Directory where the readme part files are - stored (default: ".wordpress-org/readme") - -l, --changelog-length <number> Specify the number of versions to display - before truncating the changelog. (default: - 10) - -r, --read-more <url> Specify the "Read More" url where changelogs - are published. (default: - "https://make.lifterlms.com/tag/dev") - -h, --help display help for command - -``` - -### release archive - -```bash -Usage: llms-dev release archive [options] - -Build a distribution archive (.zip) file for the project. - -Options: - -i, --inspect Automatically unzip the zip file after creation. (default: - false) - -d, --dir <dir> Directory where the generated archive file will be saved, - relative to the project root directory. (default: "dist") - -v, --verbose Output extra information with result messages. (default: - false) - -h, --help display help for command - -``` - -### release create - -```bash -Usage: llms-dev release create [options] - -Create a GitHub release and tag from a specified file or branch. - -Options: - -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. (default: - "dev-0.0.3-alpha.0.zip") - -A, --no-archive Skip creation from an archive file and use - the target --branch for release creation. - -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. (default: "Release v%s [ci - skip]") - -d, --dir <directory> Directory where distribution files are - stored. (default: "dist") - -b, --branch <branch> Target branch to use when creating the - release. (default: "release") - -l, --logfile <file> Specify the changelog file. (default: - "CHANGELOG.md") - -p, --prerelease Mark the GitHub release as a prerelease and - skip merging. - -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. (default: "prerelease") - -D, --draft Create the release as an unpublished draft - and skip merging. - -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". (default: "dev") - -n, --no-merge Disable merging before release creation. - Automatically passed when publishing a - prerelease. - -Y, --yes Skip confirmations. - -v, --verbose Output extra information with result - messages. - -h, --help display help for command - -``` - -### release prepare - -```bash -Usage: llms-dev release prepare [options] - -Prepare and build a release. - -Options: - -F, --force <version> Specify a version to use. If not specified uses - `changelog version next` to determine the version. - -p, --preid <identifier> Identifier to be used to prefix premajor, preminor, - prepatch or prerelease version increments. - -y, --yes Specify no-interaction mode. Responds "yes" to all - confirmation prompts. - -b, --build <cmd> Specify an npm script to use for the build command. - (default: "build") - -B, --no-build Disabled build script. - -h, --help display help for command - -``` - -### update-version - -```bash -Usage: llms-dev update-version [options] - -Update the project version and replace all [version] placeholders. - -Options: - -i, --increment <level> Increment the version by the specified level. Accepts: major, minor, patch, premajor, preminor, prepatch, or prerelease. (default: "patch") - -p, --preid <identifier> Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments. - -F, --force <version> Specify an explicit version instead of incrementing the current version with --increment. - -r, --replacements <replacement...>] Replacements to be made. Each replacement is an array containing a list of globs for the files to be tested and a regex used to perform the replacement. 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","(?<=(?:llms_deprecated_function|_deprecated_function|_deprecated_file\\().+)(?<=')(\\[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: )(.+)"]]) - -e, --extra-replacements <replacement...>] Additional replacements added to --replacements array. This option allows adding to the default replacements instead of overwriting them. (default: []) - -E, --exclude <glob...> Specify files to exclude from the update. (default: "./vendor/**, ./node_modules/**, ./tmp/**, ./dist/**, ./docs/**, ./packages/**") - -s, --skip-config Skip updating the version of the package.json or composer.json file. (default: true) - -h, --help display help for command - -``` - -<!-- END TOKEN(Autogenerated API docs) --> - - -## Configuration Files - -The default values for all command options may be customized by placing a YAML configuration file in the project's root directory name `.llmsdev.yml`. - -The 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 `.`. - -The following is an annotated configuration example: - -```yaml -# Configure the archive command defaults, eg `llms-dev archive`. -archive: - # Equivalent to always passing `llms-dev archive --inspect`. - inspect: true - -# Configure the the changelog add command, eg `llms-dev changelog add` -changelog.add - type: added - -``` diff --git a/packages/dev/package.json b/packages/dev/package.json deleted file mode 100644 index 10b4f14853..0000000000 --- a/packages/dev/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@lifterlms/dev", - "version": "0.0.4-alpha.0", - "description": "Developer's CLI for managing, building, and deploying LifterLMS projects.", - "author": "Team LifterLMS <dev@lifterlms.com>", - "license": "GPL-3.0-or-later", - "homepage": "https://github.com/gocodebox/lifterlms/tree/master/packages/dev", - "keywords": [ - "lifterlms", - "wordpress", - "scripts", - "utils" - ], - "repository": { - "type": "git", - "url": "https://github.com/gocodebox/lifterlms.git", - "directory": "packages/dev" - }, - "bugs": { - "url": "https://github.com/gocodebox/lifterlms/labels/package%3A%dev" - }, - "bin": { - "llms-dev": "src/index.js" - }, - "main": "src/index.js", - "publishConfig": { - "access": "public" - }, - "engines": { - "node": ">=14.14.0" - }, - "dependencies": { - "chalk": "^4.1.2", - "columnify": "^1.5.4", - "commander": "^8.2.0", - "inquirer": "^8.2.0", - "replace-in-file": "^6.3.1", - "semver": "^7.3.5", - "yaml": "^1.10.2" - }, - "scripts": { - "docgen": "llms-dev docgen" - } -} diff --git a/packages/dev/src/.eslintrc.js b/packages/dev/src/.eslintrc.js deleted file mode 100644 index 5dd3c2241f..0000000000 --- a/packages/dev/src/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - rules: { - // The CLI program utilizes console to output responses. - 'no-console': 'off', - }, -}; diff --git a/packages/dev/src/cmds/changelog/add.js b/packages/dev/src/cmds/changelog/add.js deleted file mode 100644 index 001a81aad5..0000000000 --- a/packages/dev/src/cmds/changelog/add.js +++ /dev/null @@ -1,177 +0,0 @@ -const - inquirer = require( 'inquirer' ), - chalk = require( 'chalk' ), - path = require( 'path' ), - YAML = require( 'yaml' ), - { existsSync, mkdirSync, writeFileSync } = require( 'fs' ), - { - ChangelogEntry, - getChangelogOptions, - logResult, - execSync, - isAttributionValid, - isEntryValid, - isLinkValid, - getChangelogValidationIssues, - } = require( '../../utils' ), - opts = getChangelogOptions(); - -/** - * Generate a list for the given option key. - * - * @since 0.0.1 - * - * @param {string} option Option key. - * @return {Object[]} Array of objects used for the list. - */ -function generateList( option ) { - return Object.entries( opts[ option ] ) - .map( ( [ value, desc ] ) => ( { - name: `${ value.charAt( 0 ).toUpperCase() }${ value.slice( 1 ) } [${ desc }]`, - value, - } ) ); -} - -/** - * Coerces a numeric value to a valid link value. - * - * @since 0.0.1 - * - * @param {any} link User-submitted link value. - * @return {any} The link as a valid link value if it can be coerced or the user-submitted value if it cannot. - */ -function coerceLink( link ) { - return ! isNaN( parseInt( link ) ) ? `#${ link }` : link; -} - -/** - * Create the changelog entry from the given entry object. - * - * @since 0.0.1 - * - * @param {ChangelogEntry} log Changelog entry object. - * @return {void} - */ -function writeChangelog( log ) { - const { dir } = log; - let { title } = log; - delete log.dir; - delete log.title; - - const logDir = path.join( process.cwd(), dir ); - if ( ! existsSync( logDir ) ) { - mkdirSync( logDir, { recursive: true } ); - } - - if ( log.links ) { - log.links = log.links.map( coerceLink ); - } - - const validation = getChangelogValidationIssues( log ); - if ( ! validation.valid ) { - const errs = validation.errors.map( ( err ) => `\n - ${ err }` ).join( '' ); - - logResult( `The changelog entry could not be written due to validation errors:${ errs }`, 'error' ); - - process.exit( 1 ); - } - - // Remove optional empty values. - Object.keys( log ).forEach( ( key ) => ( ! log[ key ] || ( Array.isArray( log[ key ] ) && ! log[ key ].length ) ) && delete log[ key ] ); - - // Make sure filenames are unique. - let i = 1; - title = path.join( dir, title ); - const baseTitle = title; - while ( existsSync( title + '.yml' ) ) { - title = `${ baseTitle }-${ i }`; - ++i; - } - title += '.yml'; - - writeFileSync( title, YAML.stringify( log ) ); - - logResult( `New changelog entry written to ${ chalk.bold( title ) }.`, 'success' ); -} - -const defaultTitle = execSync( `git branch --show-current`, true ).replace( '/', '_' ); - -module.exports = { - command: 'add', - description: 'Create a new changelog entry.', - options: [ - [ '-s, --significance <level>', `The semantic version significance of the change. Accepts: ${ Object.keys( opts.significance ).join( ', ' ) }.`, 'patch' ], - [ '-t, --type <type>', `The type of change. Accepts: ${ Object.keys( opts.type ).join( ', ' ) }.`, 'changed' ], - [ '-c, --comment <comment>', 'An internal-use comment to include with the changelog entry which is not published with the final changelog.' ], - [ '-l, --links <issues...>', 'Link the changelog to one or more GitHub issues. Can be provided multiple times to link to multiple issues.' ], - [ '-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.' ], - [ '-e, --entry <entry>', 'The changelog entry.' ], - [ '-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 ], - [ '-i, --interactive', 'Create the changelog interactively.', false ], - [ '-E, --use-editor', 'When creating a changelog interactively, will open an editor to write the entry, This is useful when creating multi-line entries.' ], - ], - action: ( { significance, type, comment, entry, interactive, links, attributions, dir, title, useEditor } ) => { - if ( ! entry && ! interactive ) { - logResult( 'A changelog entry is required.', 'error' ); - process.exit( 1 ); - } - - if ( interactive ) { - const commasToArray = ( arr ) => arr.split( ',' ).filter( ( part ) => part ).map( ( str ) => str.trim() ); - - const questions = [ - { - type: 'list', - name: 'significance', - message: 'Change Significance', - default: significance, - choices: generateList( 'significance' ), - pageSize: Object.keys( opts.significance ).length, - }, - { - type: 'list', - name: 'type', - message: 'Change Type', - default: significance, - choices: generateList( 'type' ), - pageSize: Object.keys( opts.type ).length, - }, - { - type: 'input', - name: 'comment', - message: 'Comment [For internal use only]', - default: comment, - }, - { - type: 'input', - name: 'links', - message: 'Linked Issues [Separate multiple issues with a comma]', - default: links ? links.join( ', ' ) : null, - filter: ( vals ) => commasToArray( vals ).map( coerceLink ), - validate: ( userVal ) => userVal.every( ( val ) => isLinkValid( val ) ) ? true : chalk.red( 'Error: Invalid link' ), - }, - { - type: 'input', - name: 'attributions', - message: 'Attributions [Separate multiple individuals with a comma]', - default: attributions ? attributions.join( ', ' ) : null, - filter: commasToArray, - validate: ( userVal ) => userVal.every( ( val ) => isAttributionValid( val ) ) ? true : chalk.red( 'Error: Invalid attribution' ), - }, - { - type: useEditor ? 'editor' : 'input', - name: 'entry', - message: 'Changelog Entry Content', - default: entry, - validate: ( val ) => isEntryValid( val ) ? true : chalk.red( 'Error: Invalid entry.' ), - }, - ]; - - inquirer.prompt( questions ) - .then( ( answers ) => writeChangelog( { ...answers, dir, title } ) ) - .catch( ( err ) => console.log( err ) ); - } else { - writeChangelog( { significance, type, comment, links, attributions, entry, dir, title } ); - } - }, -}; diff --git a/packages/dev/src/cmds/changelog/index.js b/packages/dev/src/cmds/changelog/index.js deleted file mode 100644 index 8e85870280..0000000000 --- a/packages/dev/src/cmds/changelog/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - command: 'changelog', - description: "Mange the project's changelog.", - optionsShared: [ - [ '-d, --dir <directory>', 'Directory where changelog entries are stored.', '.changelogs' ], - ], - args: [ - [ '<command>', 'The changelog subcommand to execute.' ], - ], -}; diff --git a/packages/dev/src/cmds/changelog/list.js b/packages/dev/src/cmds/changelog/list.js deleted file mode 100644 index b799626a83..0000000000 --- a/packages/dev/src/cmds/changelog/list.js +++ /dev/null @@ -1,56 +0,0 @@ -const - chalk = require( 'chalk' ), - columnify = require( 'columnify' ), - { getChangelogEntries, logResult } = require( '../../utils' ); - -module.exports = { - command: 'list', - description: 'List existing changelog entries.', - action: ( { dir } ) => { - const val = { - major: 2, - minor: 1, - patch: 0, - }; - - const entries = getChangelogEntries( dir ) - // Group by significance and then sort by title. - .sort( ( { significance: aSig, title: aTitle }, { significance: bSig, title: bTitle } ) => { - if ( val[ aSig ] < val[ bSig ] ) { - return 1; - } - if ( val[ aSig ] > val[ bSig ] ) { - return -1; - } - return aTitle > bTitle ? -1 : 1; - } ) - .map( ( entry ) => { - if ( 'major' === entry.significance ) { - Object.keys( entry ).forEach( ( key ) => entry[ key ] = chalk.bold( entry[ key ] ) ); - } else if ( 'patch' === entry.significance ) { - Object.keys( entry ).forEach( ( key ) => entry[ key ] = chalk.dim( entry[ key ] ) ); - } - return entry; - } ); - - if ( ! entries.length ) { - logResult( 'No changelog entries found.', 'warning' ); - process.exit( 0 ); - } - - console.log( columnify( - entries, - { - headingTransform: ( heading ) => chalk.bold.underline( heading.toUpperCase() ), - preserveNewLines: true, - truncate: true, - maxWidth: 18, - config: { - entry: { - maxWidth: 40, - }, - }, - }, - ) ); - }, -}; diff --git a/packages/dev/src/cmds/changelog/validate.js b/packages/dev/src/cmds/changelog/validate.js deleted file mode 100644 index d6186b3e03..0000000000 --- a/packages/dev/src/cmds/changelog/validate.js +++ /dev/null @@ -1,132 +0,0 @@ -const - chalk = require( 'chalk' ), - path = require( 'path' ), - YAML = require( 'yaml' ), - { getChangelogEntries, getChangelogValidationIssues, logResult } = require( '../../utils' ); - -/** - * Retrieve a symbol describing the status type. - * - * @since 0.0.1 - * - * @param {string} type Status type. - * @return {string} The UTF8 symbol for the requested status. - */ -function getSymbol( type ) { - let symbol = ''; - switch ( type ) { - case 'error': - symbol = chalk.red( '✘' ); - break; - case 'success': - symbol = chalk.green( '✔' ); - break; - case 'warning': - symbol = chalk.yellow( '▲' ); - break; - } - return symbol; -} - -/** - * Log a message with a status symbol prefix. - * - * @since 0.0.1 - * - * @param {string} msg The message to log. - * @param {string} type The status type. - * @return {void} - */ -function logWithSymbol( msg, type ) { - console.log( chalk.italic( ` ${ getSymbol( type ) } ${ msg }` ) ); -} - -/** - * Determine the overall status for a given changelog entry - * - * @since 0.0.1 - * - * @param {string[]} errors Array of encountered error messages. - * @param {string[]} warnings Array of encountered warning messages. - * @return {string} The overall status as a string. - */ -function determineOverallStatus( errors, warnings ) { - if ( errors.length ) { - return 'error'; - } - - if ( warnings.length ) { - return 'warning'; - } - - return 'success'; -} - -module.exports = { - command: 'validate', - description: 'Validate existing changelog entries.', - args: [ - [ '[entries...]', 'Optionally specify a list of changelog entries to validate. If omitted will validate all existing entries.' ], - ], - options: [ - [ '-f, --format [format]', 'Output format. Accepts: list, json, yaml.', 'list' ], - [ '-s, --silent', 'Skip validation output and communicate validation status only through the exit status of the command.' ], - ], - action: ( entries, { dir, silent, format } ) => { - let all; - - try { - all = getChangelogEntries( dir ); - } catch ( { name, message } ) { - logResult( `${ name }: ${ message }`, 'error' ); - if ( 'YAMLSyntaxError' === name ) { - console.log( chalk.red( ' This usually means that one or more existing changelog entries contains invalid YAML.' ) ); - } - process.exit( 1 ); - } - - // Reduce the list to only the requested entries. - if ( entries.length ) { - all = all.filter( ( { title } ) => entries.includes( path.parse( title ).name ) ); - } - - const res = {}; - - let exitStatus = 0; - - all.forEach( ( log ) => { - const validation = getChangelogValidationIssues( log, 'list' === format ), - { errors, warnings } = validation, - overallStatus = determineOverallStatus( errors, warnings ); - - if ( ! silent && 'list' === format ) { - console.log( '' ); - console.log( `${ getSymbol( overallStatus ) } ${ chalk.bold( log.title ) }` ); - console.log( chalk.dim( '-'.repeat( log.title.length + 2 ) ) ); - - if ( 'success' === overallStatus ) { - console.log( ' No issues.' ); - } - - errors.forEach( ( err ) => logWithSymbol( err, 'error' ) ); - warnings.forEach( ( warn ) => logWithSymbol( warn, 'warning' ) ); - } - - if ( 'error' === overallStatus ) { - exitStatus = 1; - } - - res[ log.title ] = validation; - } ); - - if ( ! silent ) { - if ( 'json' === format ) { - console.log( JSON.stringify( res ) ); - } else if ( 'yaml' === format ) { - console.log( YAML.stringify( res ) ); - } - } - - process.exit( exitStatus ); - }, -}; diff --git a/packages/dev/src/cmds/changelog/version.js b/packages/dev/src/cmds/changelog/version.js deleted file mode 100644 index f569c26500..0000000000 --- a/packages/dev/src/cmds/changelog/version.js +++ /dev/null @@ -1,35 +0,0 @@ -const - chalk = require( 'chalk' ), - { getNextVersion, getCurrentVersion, determineVersionIncrement, logResult } = require( '../../utils' ); - -const whichOpts = [ 'current', 'next' ]; - -module.exports = { - command: 'version', - description: 'List existing changelog entries.', - args: [ - [ '<which>', `Which version to retrieve. Accepts: ${ whichOpts.join( ', ' ) }.` ], - ], - options: [ - [ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ], - ], - action: ( which, { dir, preid } ) => { - if ( ! whichOpts.includes( which ) ) { - logResult( `Unknown argument: "${ chalk.bold( which ) }".`, 'error' ); - process.exit( 1 ); - } - - const currentVersion = getCurrentVersion(); - if ( ! currentVersion ) { - logResult( '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' ); - process.exit( 1 ); - } - - if ( 'current' === which ) { - console.log( currentVersion ); - process.exit( 0 ); - } - - console.log( getNextVersion( currentVersion, determineVersionIncrement( dir, currentVersion, preid ), preid ) ); - }, -}; diff --git a/packages/dev/src/cmds/changelog/write.js b/packages/dev/src/cmds/changelog/write.js deleted file mode 100644 index 33e5075f29..0000000000 --- a/packages/dev/src/cmds/changelog/write.js +++ /dev/null @@ -1,279 +0,0 @@ -const - path = require( 'path' ), - { readFileSync, writeFileSync, readdirSync, rmSync } = require( 'fs' ), - chalk = require( 'chalk' ), - semver = require( 'semver' ), - { - ChangelogEntry, - getNextVersion, - getCurrentVersion, - getChangelogOptions, - getChangelogValidationIssues, - getChangelogEntries, - getFileLink, - getIssueLink, - isProjectPublic, - determineVersionIncrement, - logResult, - execSync, - } = require( '../../utils' ); - -/** - * Accepts a date/time string and converts it to YYYY-MM-DD format used in changelog version titles. - * - * @since 0.0.1 - * - * @param {string|number} date Timestamp or datetime string parseable by `Date.parse()`. - * @return {string} Date string in YYYY-MM-DD format. - */ -const formatDate = ( date ) => new Date( date ).toISOString().split( 'T' )[ 0 ]; - -/** - * Retrieve the an array of lines for the changelog entry's header. - * - * @since 0.0.1 - * - * @param {string} version A semver string. - * @param {string} date A date string. - * @return {string[]} Array of lines. - */ -function getHeaderLines( version, date ) { - const lines = [ `v${ version } - ${ date }` ]; - lines.push( '-'.repeat( lines[ 0 ].length ) ); - - return lines; -} - -/** - * Retrieve the title for the changelog item's type. - * - * @since 0.0.1 - * - * @param {string} type The changelog item type key. - * @return {string} The changelog item type title. - */ -function getTypeTitle( type ) { - const map = { - added: 'New Features', - changed: 'Updates and Enhancements', - fixed: 'Bug Fixes', - deprecated: 'Deprecations', - removed: 'Breaking Changes', - dev: 'Developer Notes', - performance: 'Performance Improvements', - security: 'Security Fixes', - template: 'Updated Templates', - }; - - return `\n##### ${ map[ type ] }\n`; -} - -/** - * Formats a single changelog item. - * - * @since 0.0.1 - * @since [version] Use `getIssueLink()` for generation of issue links. - * - * @param {ChangelogEntry} args The changelog entry object. - * @param {string} args.entry The content of the changelog entry. - * @param {string} args.type Entry type. - * @param {string[]} args.attributions List of individuals attributed to the entry. - * @param {string[]} args.links of GitHub issues linked to the entry. - * @param {boolean} includeLinks Whether or not to include links. - * @return {string} The formatted changelog entry line. - */ -function formatChangelogItem( { entry, type, attributions = [], links = [] }, includeLinks ) { - entry = entry.trim(); - - // Entries should always end in a full stop. - if ( 'template' !== type && ! [ '.', '?', '!' ].includes( entry.split( '' ).reverse()[ 0 ] ) ) { - entry += '.'; - } - - let line = ''; - - // Single entry, add a bullet. - if ( ! entry.includes( '\n' ) ) { - line += '+ '; - } - - // Add the line(s). - line += entry; - - // Add formatted attribution links. - if ( attributions.length ) { - attributions = attributions.map( ( v ) => { - if ( '@' === v.charAt( 0 ) ) { - v = `[${ v }](https://github.com/${ v })`; - } - return v; - } ); - line += ` Thanks ${ new Intl.ListFormat( 'en', { style: 'long', type: 'conjunction' } ).format( attributions ) }!`; - } - - // Add issue links. - if ( includeLinks && links.length ) { - line += ' ' + links.map( ( iss ) => `[${ iss }](${ getIssueLink( iss ) })` ).join( ', ' ); - } - - return line; -} - -/** - * Retrieve a list changelog entry objects for all the template files that have been modified. - * - * Compares the current git branch against the `trunk` branch in order to find all files in the `templates/` directory - * which have been modified. - * - * @since 0.0.1 - * @since [version] Use `getFileLink()` to generate links to the template file. - * - * @param {boolean} includeLinks Whether or not the entry items should be formatted as links to the GitHub repository. - * @param {string} version A semver string. - * @return {ChangelogEntry[]} Array of changelog entry objects. - */ -function getUpdatedTemplates( includeLinks, version ) { - try { - return execSync( 'git diff --name-only trunk | grep "^templates/"', true ).split( '\n' ).map( ( template ) => { - return { - type: 'template', - entry: includeLinks ? `[${ template }](${ getFileLink( template, version ) })` : template, - }; - } ); - } catch ( e ) {} - return []; -} - -/** - * Format the changelog entry for the given version. - * - * @since 0.0.1 - * - * @param {string} version A semver string. - * @param {string} date Version release date in YYYY-MM-DD format. - * @param {ChangelogEntry[]} entries All entry objects to be included. - * @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. - * @return {string[]} Array of lines to be added to the changelog. - */ -function formatChangelogVersionEntry( version, date, entries, links ) { - const - groups = {}, - { type } = getChangelogOptions(); - - Object.keys( type ).forEach( ( groupKey ) => { - groups[ groupKey ] = []; - } ); - groups.template = []; - - // Add updated template list. - entries = [ ...entries, ...getUpdatedTemplates( links, version ) ]; - - entries.forEach( ( entry ) => { - groups[ entry.type ].push( entry ); - } ); - - const lines = [ - ...getHeaderLines( version, date ), - ]; - - Object.entries( groups ).forEach( ( [ groupType, groupEntries ] ) => { - if ( ! groupEntries.length ) { - return; - } - - lines.push( getTypeTitle( groupType ) ); - groupEntries.forEach( ( entry ) => { - lines.push( formatChangelogItem( entry, links ) ); - } ); - } ); - - return lines; -} - -/** - * Delete all changelog entry files from the changelog directory. - * - * @since 0.0.1 - * - * @param {string} dir Changelog directory. - * @return {void} - */ -function cleanupLogs( dir ) { - readdirSync( dir ).forEach( ( file ) => { - if ( file.endsWith( '.yml' ) ) { - rmSync( path.join( dir, file ) ); - } - } ); -} - -module.exports = { - command: 'write', - description: 'Write existing changelog entries to the changelog file.', - options: [ - [ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ], - [ '-F, --force <version>', 'Use the specified version string instead of determining the version based on changelog entry significance.' ], - [ '-l, --log-file <file>', 'The changelog file.', 'CHANGELOG.md' ], - [ '-d, --date <YYYY-MM-DD>', 'Changelog publication date.', formatDate( Date.now() ) ], - [ '-L, --links', 'Add GitHub links to templates and issues in changelog entries.', true === isProjectPublic() ], - [ '-n, --no-links', 'Do not add GitHub links in changelog entries. Use this option to override the --links flag.' ], - [ '-D, --dry-run', 'Output what would be written to the changelog instead of writing it to the changelog file.' ], - [ '-k, --keep-entries', 'Preserve entry files deletion after the changelog is written.' ], - ], - action: ( { dir, preid, force, logFile, date, links, dryRun, keepEntries } ) => { - try { - date = formatDate( date ); - } catch ( e ) { - logResult( 'Invalid date supplied. Please provide a date in YYYY-MM-DD format.', 'error' ); - process.exit( 1 ); - } - - const currentVersion = getCurrentVersion(); - if ( ! currentVersion ) { - logResult( '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' ); - process.exit( 1 ); - } - - const entries = getChangelogEntries( dir ); - - const areEntriesValid = entries.every( ( entry ) => { - const { valid } = getChangelogValidationIssues( entry ); - return valid; - } ); - - if ( ! areEntriesValid ) { - logResult( 'One or more invalid changelog entries were found. Please resolve all validation issues and try again.', 'error' ); - process.exit( 1 ); - } - - let version = force; - - if ( ! version ) { - version = getNextVersion( currentVersion, determineVersionIncrement( dir, currentVersion, preid ), preid ); - } else if ( ! semver.valid( version ) ) { - logResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' ); - process.exit( 1 ); - } - - logResult( `${ dryRun ? 'Generating' : 'Writing' } changelog for version ${ chalk.bold( version ) }.` ); - - const logFileContents = readFileSync( logFile, 'utf8' ); - - const - logFileParts = logFileContents.split( '\n\n' ), - [ header, ...body ] = logFileParts, - items = formatChangelogVersionEntry( version, date, entries, links ).join( '\n' ) + '\n'; - - if ( dryRun ) { - console.log( items ); - process.exit( 0 ); - } - - writeFileSync( logFile, [ header, items, ...body ].join( '\n\n' ) ); - logResult( `Changelog for version ${ chalk.bold( version ) } written.` ); - - if ( ! keepEntries ) { - logResult( `Peforming entry file cleanup`, 'warning' ); - cleanupLogs( dir ); - } - }, -}; diff --git a/packages/dev/src/cmds/docgen.js b/packages/dev/src/cmds/docgen.js deleted file mode 100644 index 257730d0e9..0000000000 --- a/packages/dev/src/cmds/docgen.js +++ /dev/null @@ -1,71 +0,0 @@ -const path = require( 'path' ), - { Command } = require( 'commander' ), // Including for the type definition. - { readFileSync, writeFileSync } = require( 'fs' ); - -/** - * Generate a doc section for the specified command. - * - * @since 0.0.1 - * - * @param {Command} command A commander command instance. - * @param {string} parentName Name of the parent command (used for subcommands). - * @return {string} Documentation section MD text. - */ -function createCommandSection( command, parentName = '' ) { - parentName = parentName ? `${ parentName } ` : ''; - - const commandName = command.name(); - let text = ''; - - if ( ! command.commands.length ) { - text = `\n### ${ parentName }${ commandName }\n\n`; - - text += '```bash\n'; - text += command.helpInformation(); - text += '\n```\n'; - } else { - command.commands.forEach( ( subcommand ) => { - text += createCommandSection( subcommand, commandName ); - } ); - } - - return text; -} - -module.exports = { - command: 'docgen', - description: 'Generates documentation for the CLI.', - action: ( env, { parent } ) => { - const readmeFile = path.join( __dirname, '../../README.md' ), - readmeContents = readFileSync( readmeFile, 'utf8' ), - startToken = '<!-- START TOKEN(Autogenerated API docs) -->', - endToken = '<!-- END TOKEN(Autogenerated API docs) -->', - docsToken = '<!-- DOCS TOKEN -->'; - - let newReadme = [], - addLine = true; - - readmeContents.split( '\n' ).forEach( ( line ) => { - if ( line === startToken ) { - newReadme.push( line ); - newReadme.push( docsToken ); - addLine = false; - } else if ( line === endToken ) { - addLine = true; - } - - if ( addLine ) { - newReadme.push( line ); - } - } ); - - newReadme = newReadme.join( '\n' ); - - let docs = ''; - parent.commands.forEach( ( command ) => { - docs += createCommandSection( command ); - } ); - - writeFileSync( readmeFile, newReadme.replace( docsToken, docs ) ); - }, -}; diff --git a/packages/dev/src/cmds/pot.js b/packages/dev/src/cmds/pot.js deleted file mode 100644 index d172dd18a1..0000000000 --- a/packages/dev/src/cmds/pot.js +++ /dev/null @@ -1,93 +0,0 @@ -const - path = require( 'path' ), - { Command } = require( 'commander' ), // Including for the type definition. - { readFileSync, writeFileSync } = require( 'fs' ), - { execSync, logResult, getProjectSlug } = require( '../utils' ), - defaultExclude = 'vendor/**, node_modules/**, tmp/**, dist/**, docs/**, src/**, tests/**, *.js.map'; - -/** - * Command: pot - * - * @since 0.0.1 - * - * @type {Object} - */ -module.exports = { - command: 'pot', - description: 'Generate i18n pot and json files using the WP-CLI.', - options: [ - [ '-d, --text-domain <text-domain>', 'Specify the text domain. Used to generate the filenames for generated files.', getProjectSlug() ], - [ '-e, --exclude <glob...>', 'Specify files to exclude from scanning.', defaultExclude ], - [ '-ee, --extra-exclude <glob...>', 'Additional files to add to the --exclude option.' ], - [ '-d, --dir <directory>', 'Output directory where generated files will be stored.', 'i18n' ], - [ '-t, --translator <translator>', 'Customize the Last Translator header.', 'Team LifterLMS <team@lifterlms.com>' ], - [ '-b, --bugs <url>', 'Customize the bug report location header.', 'https://lifterlms.com/my-account/my-tickets' ], - ], - /** - * Callback action for the pot command - * - * @since 0.0.1 - * - * @param {Object} options Command options. - * @param {string} options.textDomain Project text domain. - * @param {string} options.exclude Comma separated list of globs used to exclude files from the pot file generation. - * @param {string} options.extraExclude Extra globs to be added to exclude. - * @param {string} options.dir Output directory where the generated files will be saved. - * @param {string} options.translator Translator name and email. - * @param {string} options.bugs Bug report URL. - * @param {Command} command The command instance. - * @param {Command} command.parent The command's parent command. - * @return {void} - */ - action: ( { textDomain, exclude, extraExclude, dir, translator, bugs }, { parent } ) => { - // Ensure WP-CLI is available. - try { - execSync( 'which wp', true ); - } catch ( e ) { - logResult( 'WP-CLI must be installed in your $PATH in order to use this command.', 'error' ); - process.exit( 1 ); - } - - const - // Replace the WP CLI generator with our own generator string. - generator = `llms/dev ${ parent.version() }`, - // Get the year of the first commit to the repo. - initYear = parseInt( execSync( 'git log --reverse --format="format:%cd" --date="format:%Y" | sed -n 1p', true ) ), - currDate = new Date(), - currYear = currDate.getFullYear(), - pot = path.join( dir, `${ textDomain }.pot` ), - // Custom Headers. - headers = { - 'Last-Translator': translator, - 'Language-Team': translator, - 'Report-Msgid-Bugs-To': bugs, - 'X-Generator': generator, - }; - - // Add extra exclude globs, if defined. - if ( extraExclude ) { - exclude = exclude + ', ' + extraExclude; - } - - // Update excludes glob formatting to a format acceptable by WP CLI. - exclude = exclude.replace( /\/\*\*/g, '/' ).replace( /\.\//g, '' ); - - const cmdOpts = `--exclude="${ exclude }" --headers='${ JSON.stringify( headers ) }'`; - - // Generate the POT file. - execSync( `wp i18n make-pot ./ ${ pot } ${ cmdOpts }` ); - - // Get the original header comment. - let headerComment = execSync( `head -2 ${ pot }`, true ), - potContents = readFileSync( pot ).toString(); - - // If the initial commit date is not equal to the current year, update the copyright to include the date range. - if ( initYear !== currYear ) { - potContents = potContents.replace( headerComment, '' ); - headerComment = headerComment.replace( `(C) ${ currYear }`, `(C) ${ initYear }-${ currYear }` ); - } - - // Write the header back to the file. - writeFileSync( pot, headerComment + potContents ); - }, -}; diff --git a/packages/dev/src/cmds/readme.js b/packages/dev/src/cmds/readme.js deleted file mode 100644 index f2d2f89574..0000000000 --- a/packages/dev/src/cmds/readme.js +++ /dev/null @@ -1,94 +0,0 @@ -const - path = require( 'path' ), - chalk = require( 'chalk' ), - semver = require( 'semver' ), - { readdirSync, readFileSync, writeFileSync } = require( 'fs' ), - { parseChangelogFile, getCurrentVersion, logResult, getProjectSlug } = require( '../utils' ); - -/** - * Generate the truncated changelog section content. - * - * @since 0.0.1 - * - * @param {string} file Changelog file. - * @param {number} length Number of versions to include. - * @return {string} The truncated changelog section content. - */ -function getChangelogSection( file, length ) { - const entries = parseChangelogFile( file ), - total = entries.length, - lines = []; - - let i = 0, - added = 0; - - while ( added < length && i < total ) { - const currLog = entries[ i ]; - - // Don't add prereleases. - if ( ! semver.prerelease( currLog.version ) ) { - lines.push( `= v${ currLog.version } - ${ currLog.date } =\n\n` ); - lines.push( currLog.logs ); - - ++added; - - if ( added < length ) { - lines.push( '\n\n\n' ); - } - } - - ++i; - } - - return lines.join( '' ); -} - -module.exports = { - command: 'readme', - description: 'Create a readme.txt file suitable for the WordPress.org plugin repository.', - options: [ - [ '-o, --output-file <filename>', 'Specify the output readme file name.', 'readme.txt' ], - [ '-i, --input-file <filename>', 'Specify the input changelog file name.', 'CHANGELOG.md' ], - [ '-d, --dir <directory>', 'Directory where the readme part files are stored', '.wordpress-org/readme' ], - [ '-l, --changelog-length <number>', 'Specify the number of versions to display before truncating the changelog.', 10 ], - [ '-r, --read-more <url>', 'Specify the "Read More" url where changelogs are published.', `https://make.lifterlms.com/tag/${ getProjectSlug() }` ], - ], - action: ( { outputFile, inputFile, dir, readMore, changelogLength } ) => { - const version = getCurrentVersion(); - - // Don't generate readme files for pre-releases. - if ( semver.prerelease( version ) ) { - logResult( 'Cannot generate a readme for prereleases.', 'error' ); - process.exit( 1 ); - } - - const replacements = { - VERSION: version, - CHANGELOG_ENTRIES: getChangelogSection( inputFile, changelogLength ), - READ_MORE_LINK: readMore, - }, - files = readdirSync( dir ); - - let readme = ''; - - files.forEach( ( filename, i ) => { - const file = readFileSync( path.join( dir, filename ), 'utf8' ); - - readme += file; - - // Add newlines if it's not the last section. - if ( files.length - 1 !== i ) { - readme += '\n\n'; - } - } ); - - // Replace variables. - Object.keys( replacements ).forEach( ( varname ) => { - readme = readme.replace( `{{__${ varname }__}}`, replacements[ varname ] ); - } ); - - writeFileSync( outputFile, readme ); - - logResult( `Generated ${ chalk.bold( outputFile ) } for version ${ chalk.bold( version ) }.`, 'success' ); - }, -}; diff --git a/packages/dev/src/cmds/release/archive.js b/packages/dev/src/cmds/release/archive.js deleted file mode 100644 index ccf347e54a..0000000000 --- a/packages/dev/src/cmds/release/archive.js +++ /dev/null @@ -1,27 +0,0 @@ -const - chalk = require( 'chalk' ), - { createDistFile, execSync, logResult } = require( '../../utils' ); - -module.exports = { - command: 'archive', - description: 'Build a distribution archive (.zip) file for the project.', - options: [ - [ '-i, --inspect', 'Automatically unzip the zip file after creation.', false ], - [ '-d, --dir <dir>', 'Directory where the generated archive file will be saved, relative to the project root directory.', 'dist' ], - [ '-v, --verbose', 'Output extra information with result messages.', false ], - ], - action: ( { inspect, dir, verbose } ) => { - const distDir = `${ process.cwd() }/${ dir }`, - fileName = createDistFile( - distDir, - ! verbose, - ( msg ) => logResult( msg, 'info' ) - ); - // Unzip the archive for inspection. - if ( inspect ) { - execSync( `unzip ${ fileName }`, ! verbose, { cwd: distDir } ); - } - - logResult( `Distribution file ${ chalk.bold( fileName ) } created successfully.`, 'success' ); - }, -}; diff --git a/packages/dev/src/cmds/release/create.js b/packages/dev/src/cmds/release/create.js deleted file mode 100644 index db75d17485..0000000000 --- a/packages/dev/src/cmds/release/create.js +++ /dev/null @@ -1,164 +0,0 @@ -const - path = require( 'path' ), - { existsSync, writeFileSync } = require( 'fs' ), - chalk = require( 'chalk' ), - inquirer = require( 'inquirer' ), - { getCurrentVersion, getChangelogForVersion, getArchiveFilename, logResult, pushDistFile, execSync } = require( '../../utils' ); - -/** - * Create a temporary changelog file used to add the changelog to the GitHub release. - * - * @since 0.0.1 - * - * @param {string} version Semver string for the version being published. - * @param {string} logfile Path to the the changelog file. - * @return {string} Path to the temporary notes file. - */ -function writeTempNotesFile( version, logfile ) { - const { date, logs } = getChangelogForVersion( version, logfile ), - tmpFile = path.join( process.cwd(), 'tmp', 'release-notes.txt' ); - - let header = `v${ version } - ${ date }`; - header = `${ header }\n${ '-'.repeat( header.length ) }`; - - writeFileSync( tmpFile, `${ header }\n\n${ logs }` ); - - return tmpFile; -} - -module.exports = { - command: 'create', - description: 'Create a GitHub release and tag from a specified file or branch.', - options: [ - [ '-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() ], - [ '-A, --no-archive', 'Skip creation from an archive file and use the target --branch for release creation.' ], - [ '-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]' ], - [ '-d, --dir <directory>', 'Directory where distribution files are stored.', 'dist' ], - [ '-b, --branch <branch>', 'Target branch to use when creating the release.', 'release' ], - [ '-l, --logfile <file>', 'Specify the changelog file.', 'CHANGELOG.md' ], - [ '-p, --prerelease', 'Mark the GitHub release as a prerelease and skip merging.' ], - [ '-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' ], - [ '-D, --draft', 'Create the release as an unpublished draft and skip merging.' ], - [ '-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".', 'dev' ], - [ '-n, --no-merge', 'Disable merging before release creation. Automatically passed when publishing a prerelease.' ], - [ '-Y, --yes', 'Skip confirmations.' ], - [ '-v, --verbose', 'Output extra information with result messages.' ], - ], - action: async ( { archive, dir, commitMessage, branch, logfile, prerelease, prereleaseBranch, draft, merge, yes, verbose } ) => { - // Ensure the CLI is installed before proceeding. - try { - execSync( 'which gh', true ); - } catch ( Error ) { - logResult( 'The GitHub CLI client "gh" must be installed to use this command.', 'error' ); - process.exit( 1 ); - } - - // If there's untracked files or the working tree is dirty. - if ( execSync( 'git status -s', true ) ) { - logResult( 'The working tree must be clean before publishing.', 'error' ); - process.exit( 1 ); - } - - if ( archive ) { - archive = path.join( dir, archive ); - - if ( ! existsSync( archive ) ) { - logResult( `The distribution file ${ chalk.bold( archive ) } doesn't exist.`, 'error' ); - process.exit( 1 ); - } - } - - const version = getCurrentVersion(); - - commitMessage = commitMessage.replace( '%s', version ); - - // Use the prerelease branch when publishing a prerelease. - if ( prerelease ) { - branch = prereleaseBranch; - merge = false; - } - - // Disable merging if publishing a draft. - if ( draft ) { - merge = false; - } - - // Output information and confirm the release (unless `--yes` is passed); - if ( ! yes ) { - logResult( `About to publish a new ${ prerelease ? 'prerelease' : 'release' }${ draft ? ' (draft)' : '' }:`, 'warning' ); - logResult( `${ chalk.bold( version ) }`, ' + Version' ); - if ( archive ) { - logResult( `${ chalk.bold( archive ) }`, ' + Archive' ); - } - logResult( `${ chalk.bold( branch ) }`, ' + Branch' ); - if ( merge ) { - logResult( `${ chalk.bold( merge ) }`, ' + Merge from branch' ); - } - - yes = await inquirer.prompt( [ { - type: 'expand', - message: 'Are you sure you wish to proceed?', - name: 'yes', - choices: [ - { - key: 'y', - name: 'Yes', - value: true, - }, - { - key: 'n', - name: 'No', - value: false, - }, - ], - } ] ) - .then( ( answers ) => answers.yes ) - .catch( ( err ) => console.log( err ) ); - } - - if ( ! yes ) { - logResult( 'Release aborted.', 'error' ); - process.exit( 1 ); - } - - logResult( `Releasing version ${ chalk.bold( version ) } to the ${ chalk.bold( branch ) } branch.` ); - - // Push the distfile to the release branch. - if ( archive ) { - pushDistFile( archive, branch, commitMessage, ! verbose ); - } - - // Merge open PRs against the specified branch. - if ( merge ) { - execSync( `gh pr merge ${ merge } --merge`, true ); - } - - // Setup the release to pass to the GH CLI. - const - notesFile = writeTempNotesFile( version, logfile ), - createArgs = []; - - if ( archive ) { - createArgs.push( archive ); - } - - createArgs.push( `--title "Version ${ version }"` ); - createArgs.push( `--target ${ branch }` ); - createArgs.push( `--notes-file ${ notesFile }` ); - - if ( draft ) { - createArgs.push( '--draft' ); - } - - if ( prerelease ) { - createArgs.push( '--prerelease' ); - } - - // Create the release. - const res = execSync( `gh release create ${ version } ${ createArgs.join( ' ' ) }`, true ); - logResult( `Release v${ chalk.bold( version ) } published. Permalink: ${ chalk.underline( res ) }.` ); - - // Cleanup the tmp notesfile. - execSync( `rm ${ notesFile }` ); - }, -}; diff --git a/packages/dev/src/cmds/release/index.js b/packages/dev/src/cmds/release/index.js deleted file mode 100644 index 89920631cf..0000000000 --- a/packages/dev/src/cmds/release/index.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - command: 'release', - description: 'Prepare and deploy releases.', - args: [ - [ '<command>', 'The changelog subcommand to execute.' ], - ], -}; diff --git a/packages/dev/src/cmds/release/prepare.js b/packages/dev/src/cmds/release/prepare.js deleted file mode 100644 index 8a8f8acc10..0000000000 --- a/packages/dev/src/cmds/release/prepare.js +++ /dev/null @@ -1,103 +0,0 @@ -const - chalk = require( 'chalk' ), - inquirer = require( 'inquirer' ), - semver = require( 'semver' ), - { execSync, logResult } = require( '../../utils' ); - -/** - * Call the cli from within the cli. - * - * @since 0.0.1 - * - * @param {string} cmd CLI command and options. - * @param {boolean} silent If `true`, silence STDOUT. - * @return {?string} The STDOUT content if `silent` is `true`, otherwise `null`. - */ -function callSelf( cmd, silent = true ) { - const [ node, cli ] = process.argv; - let ret = null; - try { - ret = execSync( `${ node } ${ cli } ${ cmd }`, silent ); - } catch ( e ) { - logResult( `${ e.type }: ${ e.message }.`, 'error' ); - console.error( e ); - process.exit( 1 ); - } - return ret; -} - -/** - * Open a CLI prompt and await user confirmation. - * - * @since 0.0.1 - * - * @param {string} message Message to prompt for confirmation. - * @param {boolean} skip If true, the script is being run with `--yes` and no prompt should be made. - * @return {Promise} Returns a promise from the inquirer prompt. - */ -function prompt( message, skip = false ) { - if ( skip ) { - return true; - } - - const questions = [ - { - type: 'confirm', - name: 'confirm', - message, - default: true, - }, - ]; - - return inquirer.prompt( questions ) - .then( ( { confirm } ) => confirm ); -} - -module.exports = { - command: 'prepare', - description: 'Prepare and build a release.', - options: [ - [ '-F, --force <version>', 'Specify a version to use. If not specified uses `changelog version next` to determine the version.' ], - [ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ], - [ '-y, --yes', 'Specify no-interaction mode. Responds "yes" to all confirmation prompts.' ], - [ '-b, --build <cmd>', 'Specify an npm script to use for the build command.', 'build' ], - [ '-B, --no-build', 'Disabled build script.' ], - ], - action: async ( { force, preid, build, yes } ) => { - preid = preid ? ` --preid ${ preid }` : ''; - - // Prepare release version. - const version = force ? force : callSelf( `changelog version next${ preid }` ); - - if ( ! semver.valid( version ) ) { - logResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' ); - process.exit( 1 ); - } - - // Confirm version. - if ( ! await prompt( `Proceed using version ${ chalk.bold( version ) }?`, yes ) ) { - process.exit( 1 ); - } - - // Get the changelog. - if ( ! yes ) { - callSelf( `changelog write --dry-run --force ${ version }`, false ); - if ( ! await prompt( 'Use the above output for the changelog and build the release?' ) ) { - process.exit( 1 ); - } - } - - // Update files. - callSelf( `changelog write --force ${ version }`, false ); - - // Update version. - callSelf( `update-version --force ${ version }` ); - - // Build. - if ( build ) { - execSync( `npm run ${ build }` ); - } - - logResult( `Release preparation for version ${ chalk.bold( version ) } complete.`, 'success' ); - }, -}; diff --git a/packages/dev/src/cmds/update-version.js b/packages/dev/src/cmds/update-version.js deleted file mode 100644 index 635065620f..0000000000 --- a/packages/dev/src/cmds/update-version.js +++ /dev/null @@ -1,169 +0,0 @@ -const - chalk = require( 'chalk' ), - semver = require( 'semver' ), - columnify = require( 'columnify' ), - replace = require( 'replace-in-file' ), - { writeFileSync } = require( 'fs' ), - { getCurrentVersion, getNextVersion, logResult, getConfig, hasConfig, execSync } = require( '../utils' ); - -/** - * Update [version] placeholders via a regex against a list of file globs - * - * @since 0.0.1 - * - * @param {string} files Comma separated list of file globs. - * @param {regex} regex A regular expression to use for the replacements. - * @param {string} ignore A comma separated list of file globs to be ignored. - * @param {string} ver The semantic version string to replace the placeholder with. - * @return {Object} Replacement result object from `replace.sync()`. - */ -function updateVersions( files, regex, ignore, ver ) { - const commasToArray = ( string ) => string.split( ',' ).map( ( s ) => s.trim() ); - - files = commasToArray( files ); - - logResult( `Replacing ${ chalk.bold( files ) } using regex ${ chalk.bold( regex ) }.` ); - - const - opts = { - files, - from: new RegExp( regex, 'g' ), - to: ver, - ignore: ignore ? commasToArray( ignore ) : null, - countMatches: true, - }; - - return replace.sync( opts ); -} - -/** - * Updates the version number in the package's config file. - * - * If a package.json file is present, uses `npm version` to update the project's version. - * - * If there is no package.json, will attempt to update the `extra.llms.version` item in the - * project's composer.json file. - * - * @since 0.0.1 - * - * @param {string} ver Semantic version string. - * @return {Object} A replacement result string. - */ -function updateConfig( ver ) { - const ret = { - Matches: chalk.yellow( 1 ), - Replacements: chalk.yellow( 1 ), - }; - - if ( hasConfig( 'package' ) ) { - // Silence update errors. When updating new files and the package has already been updated the CLI throws an error which we can ignore. - try { - logResult( 'Updating package.json.' ); - execSync( `npm version --no-git-tag-version ${ ver }`, true ); - return [ - { - File: chalk.green( 'package.json' ), - ...ret, - }, - { - File: chalk.green( 'package-lock.json' ), - ...ret, - }, - ]; - } catch ( e ) {} - } else if ( hasConfig( 'composer' ) ) { - const composer = getConfig( 'composer' ); - if ( composer?.extra?.llms?.version ) { - logResult( 'Updating composer.json.' ); - composer.extra.llms.version = ver; - writeFileSync( `${ process.cwd() }/composer.json`, JSON.stringify( composer, null, 2 ) ); - return [ - { - File: chalk.green( 'composer.json' ), - ...ret, - }, - ]; - } - } - - return false; -} - -const defaultReplacements = [ - // 1. Replace [version] placeholder in all @since, @version, and @deprecated tags. - [ './**', '(?<=@(?:since|version|deprecated) +)(\\[version\\])' ], - - // 2. Replace [version] placeholder in all deprecate function methods tags. - [ './*.php,./**/*.php', '(?<=(?:llms_deprecated_function|_deprecated_function|_deprecated_file\\().+)(?<=\')(\\[version\\])(?=\')' ], - - // 3. Replace plugin metadata "Version" with current 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-]+)*))?' ], - - // 4. Replace LIFTERLMS*_VERSION constants with the current version. - [ '*lifterlms*.php', '(?<=define\\( \'(?:LLMS|LIFTERLMS).*_VERSION\', \')(.*)(?=\' \\);)' ], - - // 5. Replace theme stylesheet's version number with the current version. - [ './style.css', '(?<=Version: )(.+)' ], -]; - -module.exports = { - command: 'update-version', - description: 'Update the project version and replace all [version] placeholders.', - options: [ - [ '-i, --increment <level>', 'Increment the version by the specified level. Accepts: major, minor, patch, premajor, preminor, prepatch, or prerelease.', 'patch' ], - [ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ], - [ '-F, --force <version>', 'Specify an explicit version instead of incrementing the current version with --increment.' ], - [ '-r, --replacements <replacement...>]', 'Replacements to be made. Each replacement is an array containing a list of globs for the files to be tested and a regex used to perform the replacement. It is recommended that this argument to configured via a configuration file as opposed to being passed via a CLI flag.', defaultReplacements ], - [ '-e, --extra-replacements <replacement...>]', 'Additional replacements added to --replacements array. This option allows adding to the default replacements instead of overwriting them.', [] ], - [ '-E, --exclude <glob...>', 'Specify files to exclude from the update.', './vendor/**, ./node_modules/**, ./tmp/**, ./dist/**, ./docs/**, ./packages/**' ], - [ '-s, --skip-config', 'Skip updating the version of the package.json or composer.json file.' ], - ], - action: ( { increment, preid, exclude, force, skipConfig, replacements, extraReplacements } ) => { - const version = force ? force : getNextVersion( getCurrentVersion(), increment, preid ); - - if ( ! semver.valid( version ) ) { - logResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' ); - process.exit( 1 ); - } - - // Add extraReplacements. - replacements = [ ...replacements, ...extraReplacements ]; - - const res = []; - if ( ! skipConfig ) { - const configUpdate = updateConfig( version ); - if ( configUpdate ) { - configUpdate.forEach( ( configRes ) => res.push( configRes ) ); - } - } - - logResult( `Updating project files to version ${ chalk.bold( version ) }.` ); - - for ( let i = 0; i < replacements.length; i++ ) { - updateVersions( ...replacements[ i ], exclude, version ) - .filter( ( { hasChanged } ) => hasChanged ) - .forEach( ( update ) => { - res.push( { - File: chalk.green( update.file ), - Matches: chalk.yellow( update.numMatches ), - Replacements: chalk.yellow( update.numReplacements ), - } ); - } - ); - } - - if ( ! res.length ) { - logResult( 'No updates made.', 'warning' ); - } else { - logResult( 'Version update completed.', 'success' ); - console.log( - columnify( - res, - { - headingTransform: ( heading ) => chalk.bold.underline( heading ), - }, - ) - ); - } - }, -}; diff --git a/packages/dev/src/index.js b/packages/dev/src/index.js deleted file mode 100755 index 5be0f14ca7..0000000000 --- a/packages/dev/src/index.js +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env node - -const { argv } = process, - { readFileSync, readdirSync, lstatSync } = require( 'fs' ), - path = require( 'path' ), - { Command } = require( 'commander' ), - program = new Command(), - pkg = JSON.parse( readFileSync( path.join( __dirname, '../package.json' ), 'utf8' ) ), - { getDefault } = require( './utils' ); - -// Setup the CLI program. -program - .description( pkg.description ) - .version( pkg.version ) - .addHelpCommand( 'help [command]', 'Display help for command.' ); - -/** - * Read the contents of the specified directory, registering all as subcommands of the specified parent command. - * - * @since 0.0.1 - * - * @param {Command} parent Parent command instance. - * @param {string} dir Path to the directory where the command modules should be loaded from. - * @param {Array[]} optionsParent Array of options shared from the parent to all subcommands. - * @return {void} - */ -function registerCommands( parent, dir, optionsParent = [] ) { - readdirSync( dir ) - // Exclude index files, they're picked up automatically so we don't want to double register them. - .filter( ( file ) => 'index.js' !== file ) - .forEach( ( file ) => { - const filePath = path.join( dir, file ); - - // Register the command. - registerCommand( parent, filePath, optionsParent ); - } ); -} - -/** - * Register a command with the specified parent. - * - * @since 0.0.1 - * - * @param {Command} parent Parent command instance. - * @param {string} filePath Path to the directory where the command modules should be loaded from. - * @param {Array[]} optionsParent Array of options shared from the parent to all subcommands. - * @return {void} - */ -function registerCommand( parent, filePath, optionsParent = [] ) { - const { - command, - description, - action, - args = [], - options = [], - optionsShared = [], - help = [], - } = require( filePath ); - - const cmd = parent - .command( command ) - .description( description ); - - if ( action ) { - cmd.action( action ); - } - - args.forEach( ( cmdArgs ) => cmd.argument( ...cmdArgs ) ); - - [ ...options, ...optionsParent, ...optionsShared ].forEach( ( opts ) => { - // Attempts to parse default values from the config file. - opts[ 2 ] = getDefault( parent._name ? parent._name + '.' + command : command, opts[ 0 ], opts[ 2 ] ); - cmd.option( ...opts ); - } ); - - help.forEach( ( helpText ) => cmd.addHelpText( ...helpText ) ); - - // If it's a directory, recursively register files in the directory. - if ( lstatSync( filePath ).isDirectory() ) { - registerCommands( cmd, filePath, optionsShared ); - cmd.addHelpCommand( 'help [command]', 'Display help for command.' ); - } -} - -// Register all commands. -registerCommands( program, path.join( __dirname, 'cmds' ) ); - -// Parse incoming arguments. -program.parse( argv ); diff --git a/packages/dev/src/utils/changelog-entry.js b/packages/dev/src/utils/changelog-entry.js deleted file mode 100644 index 752abfeebe..0000000000 --- a/packages/dev/src/utils/changelog-entry.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * A changelog entry object. - * - * @typedef {Object} ChangelogEntry - * @property {string} title Title of the changelog entry. Used as the filename (excluding the extension) for the changelog file. - * @property {string} significance Entry significance. - * @property {string} type Entry type. - * @property {string} comment Internal-use comment accompanying the entry. - * @property {string[]} links List of GitHub issues linked to the entry. - * @property {string[]} attributions List of individuals attributed to the entry. - * @property {string} entry The content of the changelog entry. - */ -module.exports = { - title: '', - significance: '', - type: '', - comment: '', - links: [], - attributions: [], - entry: '', -}; diff --git a/packages/dev/src/utils/configs.js b/packages/dev/src/utils/configs.js deleted file mode 100644 index 791a61388f..0000000000 --- a/packages/dev/src/utils/configs.js +++ /dev/null @@ -1,36 +0,0 @@ -const { existsSync } = require( 'fs' ); - -/** - * Retrieve a JS object for the specified JSON config file. - * - * Returns an empty object if the config file can't be found. - * - * @since 0.0.1 - * - * @param {string} filename The JSON config filename, eg "composer" or "package". - * @return {Object} The config file as a JS object. - */ -function getConfig( filename ) { - const path = `${ process.cwd() }/${ filename }.json`; - if ( existsSync( path ) ) { - return require( path ); - } - return {}; -} - -/** - * Determines if the specified JSON config file exists. - * - * @since 0.0.1 - * - * @param {string} filename The JSON config file name, eg "composer" or "package". - * @return {boolean} Returns true if the config file exists, otherwise false. - */ -function hasConfig( filename ) { - return Object.keys( getConfig( filename ) ).length >= 1; -} - -module.exports = { - getConfig, - hasConfig, -}; diff --git a/packages/dev/src/utils/create-dist-file.js b/packages/dev/src/utils/create-dist-file.js deleted file mode 100644 index 7d9e8a1d73..0000000000 --- a/packages/dev/src/utils/create-dist-file.js +++ /dev/null @@ -1,79 +0,0 @@ -const - { existsSync } = require( 'fs' ), - getArchiveFilename = require( './get-archive-filename' ), - { getConfig } = require( './configs' ), - getProjectSlug = require( './get-project-slug' ), - execSync = require( './exec-sync' ); - -/** - * Determine if the project has composer production dependencies warranting a `composer install` during builds. - * - * @since 0.0.1 - * - * @return {boolean} Whether or not a composer install is required. - */ -function requiresComposerInstall() { - const - pkg = getConfig( 'composer' ), - keys = pkg.require ? Object.keys( pkg.require ) : [], - autoload = pkg.autoload ? Object.keys( pkg.autoload ) : []; - - // If we have autoloading enabled we need to build for production. - if ( 0 !== autoload.length ) { - return true; - } - - // Not defined or empty. - if ( 0 === keys.length ) { - return false; - } - - // Has only a php (platform) requirement. - if ( 1 === keys.length && 'php' === keys[ 0 ] ) { - return false; - } - - return true; -} - -module.exports = ( distDir, silent, log = () => {} ) => { - const name = getArchiveFilename(), - slug = getProjectSlug(), - composer = requiresComposerInstall(), - cwd = distDir; - - // If we have composer dependencies, reinstall with no dev requirements or scripts. - if ( composer ) { - log( 'Installing composer production dependencies...' ); - execSync( `composer update --no-dev --no-scripts`, silent ); - execSync( `rm composer.lock`, true ); - } - - // Empty inspected directories in the distribution directory (if any are leftover from the last run of the command). - if ( existsSync( distDir ) ) { - execSync( `rm -rf ${ slug }`, silent, { cwd } ); - } - - // Create the initial archive using composer. - execSync( `composer archive --format=zip --dir=${ distDir } --file=${ name.replace( '.zip', '' ) }`, true ); - - // Unzip the initial archive into a subdirectory matching the project's slug. - execSync( `unzip ${ name } -d ${ slug }`, silent, { cwd } ); - - // Remove the original zip file. - execSync( `rm ${ name }`, true, { cwd } ); - - // Zip up the subdirectory. - execSync( `zip -r ${ name } ${ slug }/`, silent, { cwd } ); - - // Remove the subdirectory. - execSync( `rm -rf ${ slug }/`, silent, { cwd } ); - - // If we have composer dependencies, reinstall with dev requirements when we're done. - if ( composer ) { - log( 'Reinstalling all composer dependencies...' ); - execSync( `composer update`, silent ); - } - - return name; -}; diff --git a/packages/dev/src/utils/determine-version-increment.js b/packages/dev/src/utils/determine-version-increment.js deleted file mode 100644 index 5ae3acf18f..0000000000 --- a/packages/dev/src/utils/determine-version-increment.js +++ /dev/null @@ -1,36 +0,0 @@ -const - semver = require( 'semver' ), - getChangelogEntries = require( './get-changelog-entries' ); - -/** - * Determine a version increment level. - * - * Uses existing changelog entries, the current version, and the requested preid to determine the increment to - * be made. - * - * Finds the highest significance changelog entry and uses that significance for the increment. - * - * When a preid is passed and the current version is a prerelease, significance will be disregarded and "prerelease" - * will be used for the increment. - * - * @since 0.0.1 - * @since 0.0.2 Added currentVersion and preid parameters. - * Add `pre` prefix when a `preid` is specified. - * Return `prerelease` when a `preid` is specified and `currentVersion` is already a prerelease. - * - * @param {string} dir Path to the directory where changelog entries are stored. - * @param {string} currentVersion Current project version. - * @param {?string} preid Preid identifier, eg "alpha", "beta", etc... And `null` when not requesting a prerelease. - * @return {string} A version increment string. - */ -module.exports = ( dir, currentVersion, preid = null ) => { - if ( preid && null !== semver.prerelease( currentVersion ) ) { - return 'prerelease'; - } - - const - logs = Array.from( new Set( getChangelogEntries( dir ).map( ( { significance } ) => significance ) ) ), - increment = [ 'major', 'minor', 'patch' ].find( ( level ) => logs.includes( level ) ) || 'patch'; - - return preid ? `pre${ increment }` : increment; -}; diff --git a/packages/dev/src/utils/exec-sync.js b/packages/dev/src/utils/exec-sync.js deleted file mode 100644 index baf00fa4f5..0000000000 --- a/packages/dev/src/utils/exec-sync.js +++ /dev/null @@ -1,22 +0,0 @@ -const { execSync } = require( 'child_process' ); - -/** - * Execute a command in a child process. - * - * This is a wrapper for node's child_process.execSync() with some - * quality of life improvements to reduce the necessity of specifying - * an options object to silence output. - * - * @since 0.0.1 - * - * @param {string} cmd Command to execute. - * @param {boolean} quiet If true, silences stdio output. - * @param {Object} opts Additional options object passed to `execSync()`. - * @return {string} The stdout from the command. - */ -module.exports = ( cmd, quiet = false, opts = {} ) => { - const stdio = quiet ? 'pipe' : 'inherit', - stdout = execSync( cmd, { stdio, ...opts } ); - - return quiet ? stdout.toString().trim() : ''; -}; diff --git a/packages/dev/src/utils/get-archive-filename.js b/packages/dev/src/utils/get-archive-filename.js deleted file mode 100644 index a43b401982..0000000000 --- a/packages/dev/src/utils/get-archive-filename.js +++ /dev/null @@ -1,15 +0,0 @@ -const getCurrentVersion = require( './get-current-version' ), - getProjectSlug = require( './get-project-slug' ); - -/** - * Retrieve the filename of the project's archive/distribution zip file. - * - * @since 0.0.1 - * - * @param {?string} version The version number. If not supplied uses the current version. - * @return {string} The archive filename. - */ -module.exports = ( version = null ) => { - version = version ? version : getCurrentVersion(); - return `${ getProjectSlug() }-${ version }.zip`; -}; diff --git a/packages/dev/src/utils/get-changelog-entries.js b/packages/dev/src/utils/get-changelog-entries.js deleted file mode 100644 index f5ee93936a..0000000000 --- a/packages/dev/src/utils/get-changelog-entries.js +++ /dev/null @@ -1,44 +0,0 @@ -const ChangelogEntry = require( './changelog-entry' ), - { readdirSync, readFileSync, existsSync } = require( 'fs' ), - path = require( 'path' ), - YAML = require( 'yaml' ); - -/** - * Retrieve all changelog entry files from the specified directory. - * - * This will attempt to parse all .y[a]ml files found in the specified directory. - * - * @since 0.0.1 - * - * @param {string} dir Path to the directory. - * @return {ChangelogEntry[]} Array of changelog entry objects. - */ -module.exports = ( dir ) => { - const res = []; - - if ( ! existsSync( dir ) ) { - return res; - } - - readdirSync( dir ).forEach( ( file ) => { - // Only parse valid changelog files. - if ( ! file.includes( '.yml' ) && ! file.includes( '.yaml' ) ) { - return; - } - - const log = YAML.parse( readFileSync( path.join( dir, file ), 'utf8' ) ), - { comment = '', links = '', attributions = '' } = log; - delete log.links; - delete log.comment; - delete log.attributions; - res.push( { - title: path.parse( file ).name, - ...log, - comment, - links, - attributions, - } ); - } ); - - return res; -}; diff --git a/packages/dev/src/utils/get-changelog-for-version.js b/packages/dev/src/utils/get-changelog-for-version.js deleted file mode 100644 index fe1afb5eaa..0000000000 --- a/packages/dev/src/utils/get-changelog-for-version.js +++ /dev/null @@ -1,14 +0,0 @@ -const parseChangelogFile = require( './parse-changelog-file' ); - -/** - * Retrieve a changelog for the given version. - * - * @since 0.0.1 - * - * @param {string} ver A semver string for the version to retrieve. - * @param {string} file Changelog file path. - * @return {Object|undefined} Returns the changelog version entry object or undefined if not found. - */ -module.exports = ( ver, file ) => { - return parseChangelogFile( file ).find( ( { version } ) => ver === version ); -}; diff --git a/packages/dev/src/utils/get-changelog-options.js b/packages/dev/src/utils/get-changelog-options.js deleted file mode 100644 index 844ca521a9..0000000000 --- a/packages/dev/src/utils/get-changelog-options.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = () => { - return { - significance: { - major: 'Backwards incompatible or breaking changes', - minor: 'New features or backwards-compatible deprecations', - patch: 'Backwards-compatible bug fixes', - }, - type: { - added: 'New features', - changed: 'Updates to existing features', - fixed: 'Any bug fixes', - deprecated: 'Features to be removed', - removed: 'Features that are being removed', - dev: 'Developer-related notes or changes', - performance: 'Performance improvements or fixes', - security: 'Changes related to security vulnerabilities', - }, - }; -}; diff --git a/packages/dev/src/utils/get-current-version.js b/packages/dev/src/utils/get-current-version.js deleted file mode 100644 index b1d4078a1a..0000000000 --- a/packages/dev/src/utils/get-current-version.js +++ /dev/null @@ -1,22 +0,0 @@ -const { getConfig } = require( './configs' ); - -/** - * Retrieve the current version number of the project - * - * @since 0.0.1 - * - * @return {string} A semver string or an empty string if no version could be parsed. - */ -module.exports = () => { - const npm = getConfig( 'package' ); - if ( npm.version ) { - return npm.version; - } - - const composer = getConfig( 'composer' ); - if ( composer?.extra?.llms?.version ) { - return composer.extra.llms.version; - } - - return ''; -}; diff --git a/packages/dev/src/utils/get-default.js b/packages/dev/src/utils/get-default.js deleted file mode 100644 index 1d800da4b1..0000000000 --- a/packages/dev/src/utils/get-default.js +++ /dev/null @@ -1,74 +0,0 @@ -const - path = require( 'path' ), - parseYaml = require( 'yaml' ).parse, - { existsSync, readFileSync } = require( 'fs' ); - -/** - * Find the config file. - * - * Looks in the project's root directory for .llmsdev.yml or .llmsdev.yaml. - * - * @since 0.0.1 - * - * @return {string} Returns the full path to the config file or an empty string if none can be found. - */ -function getConfigFilePath() { - const basePath = path.join( process.cwd(), '.llmsdev' ); - - let configFilePath = ''; - - [ '.yml', '.yaml' ].some( ( ext ) => { - const testPath = basePath + ext; - if ( existsSync( testPath ) ) { - configFilePath = testPath; - return true; - } - - return false; - } ); - - return configFilePath; -} - -/** - * Load the gloabl config file. - * - * @since 0.0.1 - * - * @return {Object} Returns the parsed config file as a JS object or an empty object if none found. - */ -function loadConfigFile() { - const filePath = getConfigFilePath(); - if ( ! filePath ) { - return {}; - } - - return parseYaml( readFileSync( filePath, 'utf8' ) ); -} - -/** - * Get a default value for a given command and option. - * - * @since - * - * @param {string} command Name of the command. When accessing subcommands the command name will be "parent.subcommand". - * @param {string} setting The option value, eg: "-v --verbose" or "-m --mode <mode>". - * This string will be parsed and use the value following the two hyphens. - * Using the examples the value from the config would accept the value of "verbose" or "mode". - * @param {any} defaultValue The default value as specified in the command options. - * @return {any} The default value of the option. - */ -module.exports = ( command, setting, defaultValue = undefined ) => { - setting = setting.split( ' ' )[ 1 ].replace( '--', '' ); - - const config = loadConfigFile(); - if ( - ! config || - 0 === Object.keys( config ) || - undefined === config[ command ] || - undefined === config[ command ][ setting ] ) { - return defaultValue; - } - - return config[ command ][ setting ]; -}; diff --git a/packages/dev/src/utils/get-next-version.js b/packages/dev/src/utils/get-next-version.js deleted file mode 100644 index ea3acaeef2..0000000000 --- a/packages/dev/src/utils/get-next-version.js +++ /dev/null @@ -1,31 +0,0 @@ -const semver = require( 'semver' ); - -/** - * Determine the next version for a release given the current version and an increment level + preid. - * - * This function is a wrapper around `semver.inc()` with some modifications: - * + If a `preid` is provided, `pre` will automatically be added to `increment` (unless it's already been added). - * + When creating the first prerelease, eg 1.0.0 -> 2.0.0-beta.1, this function skips beta.0 and makes beta.1. - * - * @since 0.0.1 - * @since 0.0.2 Only add "pre" to the increment if it is already added. - * - * @param {string} version Version to increment. - * @param {string} increment Increment level: major, premajor, minor, preminor, patch, prepatch, or prerelease. - * @param {string} preid Prerelease identifier when using `pre*` increment levels. EG: "alpha", "beta", "rc". - * @return {string} The incremented string. - */ -module.exports = ( version, increment, preid = null ) => { - increment = preid && ! increment.startsWith( 'pre' ) ? `pre${ increment }` : increment; - - // When incrementing a prerelease we want to skip versions like "-beta.0" and go right to "-beta.1". - if ( increment.includes( 'pre' ) ) { - const prever = semver.inc( version, increment, preid ); - if ( 0 === semver.prerelease( prever ).reverse()[ 0 ] ) { - version = prever; - increment = 'prerelease'; - } - } - - return semver.inc( version, increment, preid ); -}; diff --git a/packages/dev/src/utils/get-project-privacy.js b/packages/dev/src/utils/get-project-privacy.js deleted file mode 100644 index 887a0079d9..0000000000 --- a/packages/dev/src/utils/get-project-privacy.js +++ /dev/null @@ -1,63 +0,0 @@ -const - getProjectSlug = require( './get-project-slug' ), - execSync = require( './exec-sync' ); - -/** - * Get the project's repo privacy status. - * - * Uses the GitHub CLI client (gh) to lookup the project's status via the GitHub api. If the API - * encounters errors (like a 404 or an authentication error) it will fail silently and result - * in an "unknown" response. - * - * @since 0.0.2 - * - * @return {string} Returns 'public' or 'private'. If the repo cannot be found, returns 'unknown'. - */ -function getProjectPrivacy() { - let status = 'unknown'; - - try { - const res = JSON.parse( execSync( `gh api repos/gocodebox/${ getProjectSlug() }`, true ) ); - status = res.private ? 'private' : 'public'; - } catch ( e ) {} - - return status; -} - -/** - * Determine if the project is private. - * - * @since 0.0.2 - * - * @return {boolean | undefined} Returns `true` for private repos, `false` for public repos, and `undefined` for unknown repos. - */ -function isProjectPrivate() { - const privacy = getProjectPrivacy(); - if ( 'unknown' === privacy ) { - return undefined; - } - return 'private' === privacy; -} - -/** - * Determine if the project is private. - * - * @since 0.0.2 - * - * @return {boolean | undefined} Returns `false` for private repos, `true` for public repos, and `undefined` for unknown repos. - */ -function isProjectPublic() { - const privacy = getProjectPrivacy(); - if ( 'unknown' === privacy ) { - return undefined; - } - return 'public' === privacy; -} - -module.exports = { - - isProjectPrivate, - isProjectPublic, - getProjectPrivacy, - -}; diff --git a/packages/dev/src/utils/get-project-slug.js b/packages/dev/src/utils/get-project-slug.js deleted file mode 100644 index 83886d6b28..0000000000 --- a/packages/dev/src/utils/get-project-slug.js +++ /dev/null @@ -1,16 +0,0 @@ -const { basename } = require( 'path' ); - -/** - * Retrieve the package's "slug". - * - * This will always be equal to the directory name. - * - * For example "lifterlms" or "lifterlms-integration-woocommerce". - * - * @since 0.0.1 - * - * @return {string} The project's slug. - */ -module.exports = () => { - return basename( process.cwd() ); -}; diff --git a/packages/dev/src/utils/index.js b/packages/dev/src/utils/index.js deleted file mode 100644 index 4c2d26e601..0000000000 --- a/packages/dev/src/utils/index.js +++ /dev/null @@ -1,52 +0,0 @@ -const - ChangelogEntry = require( './changelog-entry' ), - createDistFile = require( './create-dist-file' ), - determineVersionIncrement = require( './determine-version-increment' ), - execSync = require( './exec-sync' ), - getArchiveFilename = require( './get-archive-filename' ), - getChangelogEntries = require( './get-changelog-entries' ), - getChangelogForVersion = require( './get-changelog-for-version' ), - getChangelogOptions = require( './get-changelog-options' ), - getCurrentVersion = require( './get-current-version' ), - getDefault = require( './get-default' ), - getNextVersion = require( './get-next-version' ), - { isProjectPrivate, isProjectPublic, getProjectPrivacy } = require( './get-project-privacy' ), - getProjectSlug = require( './get-project-slug' ), - { getConfig, hasConfig } = require( './configs' ), - { getFileLink, getRepoLink, getIssueLink } = require( './repo-links' ), - logResult = require( './log-result' ), - parseChangelogFile = require( './parse-changelog-file' ), - parseIssueString = require( './parse-issue-string' ), - pushDistFile = require( './push-dist-file' ), - { isAttributionValid, isEntryValid, isLinkValid, getChangelogValidationIssues } = require( './validate-changelog' ); - -module.exports = { - ChangelogEntry, - createDistFile, - determineVersionIncrement, - execSync, - getArchiveFilename, - getChangelogEntries, - getChangelogForVersion, - getChangelogOptions, - getConfig, - getCurrentVersion, - getDefault, - getFileLink, - getIssueLink, - getNextVersion, - getProjectSlug, - getRepoLink, - isProjectPrivate, - isProjectPublic, - getProjectPrivacy, - hasConfig, - logResult, - isAttributionValid, - isEntryValid, - isLinkValid, - getChangelogValidationIssues, - parseChangelogFile, - parseIssueString, - pushDistFile, -}; diff --git a/packages/dev/src/utils/log-result.js b/packages/dev/src/utils/log-result.js deleted file mode 100644 index f153492d56..0000000000 --- a/packages/dev/src/utils/log-result.js +++ /dev/null @@ -1,34 +0,0 @@ -const chalk = require( 'chalk' ); - -/** - * Log a result to the console - * - * @since 0.0.1 - * - * @param {string} msg Message to log. - * @param {string} type Message type. Accepts success, warning, error, or info. - * @return {void} - */ -module.exports = ( msg, type = 'info' ) => { - msg = chalk.bold( type.charAt( 0 ).toUpperCase() + type.slice( 1 ) ) + ': ' + msg; - - switch ( type ) { - case 'success': - msg = chalk.green( msg ); - break; - - case 'warning': - msg = chalk.yellow( msg ); - break; - - case 'error': - msg = chalk.red( msg ); - break; - - case 'info': - msg = chalk.blue( msg ); - break; - } - - console.log( msg ); -}; diff --git a/packages/dev/src/utils/parse-changelog-file.js b/packages/dev/src/utils/parse-changelog-file.js deleted file mode 100644 index f86d1a53e8..0000000000 --- a/packages/dev/src/utils/parse-changelog-file.js +++ /dev/null @@ -1,88 +0,0 @@ -const { readFileSync } = require( 'fs' ); - -/** - * Retrieve an entry object stub with the given date and version. - * - * @since 0.0.1 - * - * @param {string} date Release date in `YYYY-MM-DD` format. - * @param {string} version A semver string. - * @return {Object} Entry object. - */ -function getEntryObject( date, version ) { - return { - date, - version, - logs: [], - }; -} - -/** - * Convert an entry item list into a string, preserving newlines within the list but stripping them from the start and end. - * - * @since 0.0.1 - * - * @param {string[]} entry Array of lines. - * @return {string} Joined entry string. - */ -function finalizeEntry( entry ) { - // Trim newlines from the beginning of the entry list. - if ( ! entry.logs[ 0 ] ) { - entry.logs.splice( 0, 1 ); - } - - // Trim trailing nnewlines from the end of the entry list. - while ( ! entry.logs[ entry.logs.length - 1 ] ) { - entry.logs.splice( entry.logs.length - 1, 1 ); - } - - // Join them all together with a new line. - entry.logs = entry.logs.join( '\n' ); - - return entry; -} - -/** - * Convert a changelog file to a JSON object - * - * @since 0.0.1 - * - * @param {string} file Path to the changelog MD file. - * @return {Object[]} Changelog as an array of JSON objects. - */ -module.exports = ( file ) => { - const changelog = readFileSync( file, 'utf8' ), - lines = changelog.split( '\n' ), - logs = [], - regex = /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})/; - - // Remove title, title underline, and first blank line. - lines.splice( 0, 3 ); - - let currEntry = {}; - - lines.forEach( ( line ) => { - if ( line.startsWith( '====' ) || line.startsWith( '----' ) ) { - return; - } - - const parsed = regex.exec( line ); - - // This is a header line. - if ( parsed ) { - // Before we start processing the next log item, finalize what we've compiled from the previous. - if ( currEntry.logs && currEntry.logs.length ) { - logs.push( finalizeEntry( currEntry ) ); - } - - currEntry = getEntryObject( parsed.groups.date, parsed.groups.version ); - } else { - currEntry.logs.push( line ); - } - } ); - - // 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. - logs.push( finalizeEntry( currEntry ) ); - - return logs; -}; diff --git a/packages/dev/src/utils/parse-issue-string.js b/packages/dev/src/utils/parse-issue-string.js deleted file mode 100644 index 9f3358a779..0000000000 --- a/packages/dev/src/utils/parse-issue-string.js +++ /dev/null @@ -1,35 +0,0 @@ -const getProjectSlug = require( './get-project-slug' ); - -/** - * A GitHub-style issue reference object. - * - * @typedef {Object} GitHubIssueRef - * @property {string} org The GitHub organization slug, eg "gocodebox". - * @property {string} repo The GitHub repo slug, eg "lifterlms". - * @property {string} num The issue number, eg "1234". - */ - -/** - * Parses a GitHub-style issue reference string into it's parts. - * - * @since [version] - * - * @param {string} issue A GitHub-style issue reference string. Formatted as either "#123" or "organization/repository#123". - * @return {GitHubIssueRef} An issue object. - */ -module.exports = ( issue ) => { - let org = 'gocodebox', - repo = getProjectSlug(), - num = ''; - - // Is an external reference. - if ( issue.includes( '/' ) ) { - const split = issue.split( '/' ); - org = split[ 0 ]; - [ repo, num ] = split[ 1 ].split( '#' ); - } else { - num = issue.slice( 1 ); - } - - return { org, repo, num }; -}; diff --git a/packages/dev/src/utils/push-dist-file.js b/packages/dev/src/utils/push-dist-file.js deleted file mode 100644 index ef1940b4f3..0000000000 --- a/packages/dev/src/utils/push-dist-file.js +++ /dev/null @@ -1,56 +0,0 @@ -// Deps. -const - execSync = require( './exec-sync' ), - getProjectSlug = require( './get-project-slug' ); - -/** - * Commit and push a specified zip file to a git branch. - * - * This is used, primarily, to publish the distribution archive of a project - * to the "release" branch which is used to create and publish installable releases. - * - * @since 0.0.1 - * @since 0.0.2 OSX compatibility: don't use `xargs -d`. - * - * @param {string} distFile Distribution file used as the source of the commit. - * @param {string} branch Branch to commit and push to. - * @param {string} message Commit message. - * @param {boolean} silent Whether or not to output child process stdout. - * @return {void} - */ -module.exports = ( distFile, branch, message, silent = true ) => { - const slug = getProjectSlug(); - - execSync( 'mkdir -p ./tmp' ); - - const - cwd = process.cwd() + '/tmp/git', - url = execSync( 'git config --get remote.origin.url', true ); - - // Clone the repo into a temp directory. - execSync( `git clone ${ url } ${ cwd }`, silent ); - - // Checkout to the publication branch. - execSync( `git checkout -b ${ branch }`, silent, { cwd } ); - - // Empty everything except the git directory. - execSync( `mv .git ../ && cd ../ && rm -rf ./git && mkdir git && mv .git ./git && cd git`, silent, { cwd } ); - - // Extract the distribution file. - execSync( `unzip ${ distFile } -d ./tmp/git/`, silent ); - - // Move all the contents into the publication branch. - execSync( `mv ./${ slug }/* ./ && rm -rf ${ slug }/`, silent, { cwd } ); - - // Add all files. - execSync( `git add -A`, silent, { cwd } ); - - // Commit. - execSync( `git commit --allow-empty -m "${ message }"`, silent, { cwd } ); - - // Force push. - execSync( `git push origin ${ branch } -f`, silent, { cwd } ); - - // Remove temp repo dir. - execSync( `rm -rf ./tmp/git`, silent ); -}; diff --git a/packages/dev/src/utils/repo-links.js b/packages/dev/src/utils/repo-links.js deleted file mode 100644 index d9d2b7453e..0000000000 --- a/packages/dev/src/utils/repo-links.js +++ /dev/null @@ -1,49 +0,0 @@ -const getProjectSlug = require( './get-project-slug' ), - parseIssueString = require( './parse-issue-string' ); - -/** - * Retrieves a link to the specified file on the project's GitHub repository. - * - * @since [version] - * - * @param {string} path Path to the file (relative to the root directory), eg: "includes/file.php" or "main.php". - * @param {string} branch Branch or version number. Defaults to "trunk". - * @return {string} The full URL to a file on the project's GitHub repository. - */ -function getFileLink( path, branch = 'trunk' ) { - return `${ getRepoLink() }/blob/${ branch }/${ path }`; -} - -/** - * Retrieves a link to the specified issue on the project's GitHub repository. - * - * @since [version] - * - * @param {string} issue A GitHub-style issue reference string. Formatted as either "#123" or "organization/repository#123". - * @return {string} The full URL to the specified issue. - */ -function getIssueLink( issue ) { - const { org, repo, num } = parseIssueString( issue ); - return `${ getRepoLink( repo, org ) }/issues/${ num }`; -} - -/** - * Retrieves the base link to a project's GitHub repository. - * - * @since [version] - * - * @param {string} project The project slug. - * @param {string} org The project organization. - * @return {string} The full URL to the current project's GitHub repository. - */ -function getRepoLink( project, org ) { - project = project || getProjectSlug(); - org = org || 'gocodebox'; - return `https://github.com/${ org }/${ project }`; -} - -module.exports = { - getFileLink, - getIssueLink, - getRepoLink, -}; diff --git a/packages/dev/src/utils/validate-changelog.js b/packages/dev/src/utils/validate-changelog.js deleted file mode 100644 index 9af4f52320..0000000000 --- a/packages/dev/src/utils/validate-changelog.js +++ /dev/null @@ -1,219 +0,0 @@ -require( 'url' ); - -const - ChangelogEntry = require( './changelog-entry' ), - chalk = require( 'chalk' ), - getChangelogOptions = require( './get-changelog-options' ); - -/** - * Highlights text depending on the formatting request - * - * When formatting is disabled, the text is wrapped in quotes. - * - * When formatting is enabled, the text will be quoted and emboldened. - * - * @since 0.0.1 - * - * @param {string} text The text to highlight. - * @param {boolean} formatting Whether or not rich formatting should be used. - * @return {string} The highlighted text. - */ -function highlight( text, formatting = true ) { - text = formatting ? chalk.bold( text ) : text; - return `"${ text }"`; -} - -/** - * Determines if an attribution string is valid. - * - * Attributions are valid in the following formats: - * + GitHub username reference: @thomasplevy - * + Markdown link: [Jeffrey Lebowski](https://elduderino.geocites.com/) - * - * @since 0.0.1 - * - * @param {string} attr User-submitted attribution string. - * @return {boolean} Returns `true` if the attribution string is valid, otherwise `false`. - */ -function isAttributionValid( attr ) { - attr = attr.toString(); - - const firstChar = attr.charAt( 0 ); - - // GitHub username. - if ( '@' === firstChar ) { - return true; - } - - const - match = attr.match( /\[[^\]]*\]\(([^)]*)\)*/ ); - - if ( ! match ) { - return false; - } - - try { - new URL( match[ 1 ] ); - return true; - } catch ( e ) {} - - return false; -} - -/** - * Determine if a supplied link is valid - * - * Links are valid in the following formats: - * + GitHub issue reference in the current repo: #12345 - * + GitHub issue reference to another repo: organization/repository#12345 - * - * @since 0.0.1 - * - * @param {string} link User-submitted link string. - * @return {boolean} Returns `true` if the link is valid and false otherwise. - */ -function isLinkValid( link ) { - // Force values to a string. - link = link.toString(); - - const isValidHash = ( hash ) => ! isNaN( parseInt( hash.slice( 1 ) ) ); - - let valid = false; - - // Valid hash string, eg "#123". - if ( '#' === link.charAt( '0' ) ) { - valid = isValidHash( link ); - } else { - // Valid reference to another repo, eg: "org/repo#123". - const split = link.split( '/' ); - if ( 2 !== split.length ) { - valid = false; - } else { - valid = split[ 1 ].includes( '#' ) && isValidHash( '#' + split[ 1 ].split( '#' )[ 1 ] ); - } - } - - return valid; -} - -/** - * Determine if the supplied entry is valid. - * - * A valid entry can be single or multiple lines. - * - * A single-line must: - * + Begin with a capital letter (the bullet character should be omitted). - * + End with a full stop: period, question mark, or exclamation point. - * - * A multi-line: - * + Each line must start with a plus sign bullet character: `+`. - * + A single space must follow the bullet. - * + The remaining portion of each line follows the same rules as a single-line entry. - * + Additionally, a line may end in a colon. - * - * @since 0.0.1 - * - * @param {string} entry The changelog entry string. - * @return {boolean} Returns `true` if the entry is valid and `false` otherwise. - */ -function isEntryValid( entry ) { - const singleLineRegex = /^[A-Z].*[.!?]$/, - multiLineRegex = /^( )?[+] [A-Z].*[:.!?]$/; - - const test = ( line, regex ) => null !== line.match( regex ); - - if ( entry.includes( '\n' ) ) { - return entry.split( '\n' ).filter( ( line ) => line ).every( ( line ) => test( line, multiLineRegex ) ); - } - - return test( entry, singleLineRegex ); -} - -/** - * Object describing changelog validation issues found with a specified ChangelogEntry. - * - * @typedef {Object} ChangelogValidationIssues - * @property {boolean} valid Whether or not validation errors were found. - * @property {string[]} errors Array of validation error messages. - * @property {string[]} warnings Array of validation warning messages. - */ - -/** - * Retrieve a list of changelog validation issues. - * - * @since 0.0.1 - * - * @param {ChangelogEntry} logEntry The changelog entry object to validate. - * @param {boolean} formatting Whether or not messages should include formatting (colors and bold). - * @return {ChangelogValidationIssues} Encountered validation issues - */ -function getChangelogValidationIssues( logEntry, formatting = true ) { - const options = getChangelogOptions(), - errors = [], - warnings = []; - - // Check required fields. - [ 'significance', 'type', 'entry' ].forEach( ( key ) => { - if ( ! logEntry[ key ] ) { - errors.push( `Missing required field: ${ highlight( key, formatting ) }.` ); - } - } ); - - // Validate the entry. - if ( logEntry.entry && ! isEntryValid( logEntry.entry ) ) { - errors.push( `The submitted entry text did not pass validation.` ); - } - - // Validate enum values. - [ 'significance', 'type' ].forEach( ( key ) => { - if ( logEntry[ key ] && ! Object.keys( options[ key ] ).includes( logEntry[ key ] ) ) { - errors.push( `Invalid value ${ highlight( logEntry[ key ], formatting ) } supplied for field: ${ highlight( key, formatting ) }.` ); - } - } ); - - // Warn when encountering extra/non-standard keys. - Object.keys( logEntry ) - // Expected keys. - .filter( ( k ) => ! Object.keys( ChangelogEntry ).includes( k ) ) - .forEach( ( key ) => { - warnings.push( `Unexpected key: ${ highlight( key, formatting ) }.` ); - } ); - - // Ensure array fields are arrays. - [ 'links', 'attributions' ].forEach( ( key ) => { - if ( logEntry[ key ] && ! Array.isArray( logEntry[ key ] ) ) { - errors.push( `The ${ highlight( key, formatting ) } field must be an array.` ); - } - } ); - - // Validate all links. - if ( Array.isArray( logEntry.links ) ) { - logEntry.links.forEach( ( link ) => { - if ( ! isLinkValid( link ) ) { - errors.push( `The link ${ highlight( link, formatting ) } is invalid.` ); - } - } ); - } - - // Validate all attributions. - if ( Array.isArray( logEntry.attributions ) ) { - logEntry.attributions.forEach( ( attribution ) => { - if ( ! isAttributionValid( attribution ) ) { - errors.push( `The attribution ${ highlight( attribution, formatting ) } is invalid.` ); - } - } ); - } - - return { - valid: 0 === errors.length, - errors, - warnings, - }; -} - -module.exports = { - isAttributionValid, - isEntryValid, - isLinkValid, - getChangelogValidationIssues, -}; diff --git a/packages/dev/test/utils/configs.test.js b/packages/dev/test/utils/configs.test.js deleted file mode 100644 index c0cf1d7c7f..0000000000 --- a/packages/dev/test/utils/configs.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const { getConfig, hasConfig } = require( '../../src/utils' ); - -describe( 'hasConfig', () => { - it( 'should return `true` if the project has the specified config file', () => { - expect( hasConfig( 'package' ) ).toBe( true ); - expect( hasConfig( 'composer' ) ).toBe( true ); - } ); - - it( 'should return `false` if the project does not have the config or the config is empty', () => { - expect( hasConfig( 'fake' ) ).toBe( false ); - } ); -} ); - -describe( 'getConfig', () => { - it( 'should return the config file as a JS object', () => { - const expectedPkg = require( '../../../../package.json' ), - expectedComposer = require( '../../../../composer.json' ); - - expect( getConfig( 'package' ) ).toStrictEqual( expectedPkg ); - expect( getConfig( 'composer' ) ).toStrictEqual( expectedComposer ); - } ); - - it( 'should return an empty object if the project does not have the specified config', () => { - expect( getConfig( 'fake' ) ).toStrictEqual( {} ); - } ); -} ); diff --git a/packages/dev/test/utils/determine-version-increment.test.js b/packages/dev/test/utils/determine-version-increment.test.js deleted file mode 100644 index 2bdb5a9531..0000000000 --- a/packages/dev/test/utils/determine-version-increment.test.js +++ /dev/null @@ -1,55 +0,0 @@ -jest.mock( '../../src/utils/get-changelog-entries' ); - -const getChangelogEntries = require( '../../src/utils/get-changelog-entries' ), - determineVersionIncrement = require( '../../src/utils/determine-version-increment' ); - -let mockedEntries; - -/** - * Create mock entries for the return of getChangelogEntries(). - * - * @since 0.0.2 - * - * @param {string} significance Highest significance to add to the list. - * @return {Object[]} Array of partial log entry objects. - */ -function setupMockEntries( significance ) { - const entries = []; - - for ( let i = 0; i <= 3; i++ ) { - entries.push( { significance: 'patch' } ); - } - - entries.push( { significance } ); - - // Shuffle entries. - return entries.slice().sort( () => Math.random() - 0.5 ); -} - -getChangelogEntries.mockImplementation( () => mockedEntries ); - -describe( 'determineVersionIncrement', () => { - it.each( [ 'major', 'minor', 'patch' ] )( 'Should return "%s" when it is the highest significance', ( significance ) => { - mockedEntries = setupMockEntries( significance ); - expect( determineVersionIncrement( 'dir', '1.0.0' ) ).toBe( significance ); - } ); - - it.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 ) => { - mockedEntries = setupMockEntries( significance ); - expect( determineVersionIncrement( 'dir', '1.0.0', 'beta' ) ).toBe( `pre${ significance }` ); - } ); - - const testData = [ - [ '1.0.0-beta.1', 'beta' ], - [ '1.3.0-rc.3', 'alpha' ], - [ '5.2.0-alpha.23', 'rc' ], - ]; - it.each( testData )( 'Should return "prerelease" for currentVersion=%s and preid=%s', ( currVersion, preid ) => { - expect( determineVersionIncrement( 'dir', currVersion, preid ) ).toBe( 'prerelease' ); - } ); - - it( 'Should return "patch" when no entries can be found', () => { - mockedEntries = []; - expect( determineVersionIncrement( 'dir', '1.0.0' ) ).toBe( 'patch' ); - } ); -} ); diff --git a/packages/dev/test/utils/exec-sync.test.js b/packages/dev/test/utils/exec-sync.test.js deleted file mode 100644 index 1ab3e53764..0000000000 --- a/packages/dev/test/utils/exec-sync.test.js +++ /dev/null @@ -1,39 +0,0 @@ -// eslint-disable-next-line camelcase -const childProcess = require( 'child_process' ), - { execSync } = require( '../../src/utils' ); - -jest.mock( 'child_process' ); - -// Mock submitted command options. -let cmdOpts = {}; - -childProcess.execSync.mockImplementation( ( cmd, opts ) => cmdOpts = opts ); - -/** - * Test execSync utility. - * - * Note this test doesn't attempt to ensure the output is correct as it assumes - * that child_proccess.execSync() works as expected. This tests only our logic - * to ensure that we properly merge the options passed to execSync. - */ -describe( 'execSync', () => { - // Reset mocked options. - beforeEach( () => { - cmdOpts = {}; - } ); - - it( 'should output command output by default', () => { - execSync( 'echo "HELLO"' ); - expect( cmdOpts ).toStrictEqual( { stdio: 'inherit' } ); - } ); - - it( 'should silence output', () => { - execSync( 'echo "HELLO"', true ); - expect( cmdOpts ).toStrictEqual( { stdio: 'pipe' } ); - } ); - - it( 'should add accept additional arguments', () => { - execSync( 'echo "HELLO"', true, { timeout: 1000 } ); - expect( cmdOpts ).toStrictEqual( { stdio: 'pipe', timeout: 1000 } ); - } ); -} ); diff --git a/packages/dev/test/utils/get-archive-filename.test.js b/packages/dev/test/utils/get-archive-filename.test.js deleted file mode 100644 index 8bb52a4d94..0000000000 --- a/packages/dev/test/utils/get-archive-filename.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const { getArchiveFilename } = require( '../../src/utils' ); - -describe( 'getArchiveFilename', () => { - it( 'should assume the current version when no version is specified', () => { - const { version } = require( '../../../../package.json' ); - expect( getArchiveFilename() ).toBe( `lifterlms-${ version }.zip` ); - } ); - - it( 'should add the specified version', () => { - expect( getArchiveFilename( '9.9.9' ) ).toBe( 'lifterlms-9.9.9.zip' ); - } ); -} ); diff --git a/packages/dev/test/utils/get-changelog-for-version.test.js b/packages/dev/test/utils/get-changelog-for-version.test.js deleted file mode 100644 index e3f913afeb..0000000000 --- a/packages/dev/test/utils/get-changelog-for-version.test.js +++ /dev/null @@ -1,15 +0,0 @@ -const { getChangelogForVersion } = require( '../../src/utils' ); - -describe( 'getChangelogForVersion', () => { - it.each( [ '5.4.0', '3.0.0', '1.0.1' ] )( 'should retrieve the changelog for the existing version %s', ( version ) => { - const entry = getChangelogForVersion( version, process.cwd() + '/CHANGELOG.md' ); - expect( typeof entry ).toBe( 'object' ); - expect( entry.version ).toStrictEqual( version ); - expect( Object.keys( entry ) ).toStrictEqual( [ 'date', 'version', 'logs' ] ); - } ); - - it( 'should return undefined for non-existent versions', () => { - const entry = getChangelogForVersion( '0.0.1', process.cwd() + '/CHANGELOG.md' ); - expect( entry ).toBeUndefined(); - } ); -} ); diff --git a/packages/dev/test/utils/get-next-version.test.js b/packages/dev/test/utils/get-next-version.test.js deleted file mode 100644 index 943c935917..0000000000 --- a/packages/dev/test/utils/get-next-version.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const - { getNextVersion } = require( '../../src/utils' ); - -describe( 'getNextVersion', () => { - describe( 'increment=patch', () => { - const testData = [ - [ '1.0.0', '1.0.1' ], - [ '5.1.2', '5.1.3' ], - [ '0.12.9', '0.12.10' ], - [ '1.0.0-beta.1', '1.0.0' ], - [ '999.9.9-rc.1', '999.9.9' ], - ]; - it.each( testData )( 'Should increment %s to %s', ( current, next ) => { - expect( getNextVersion( current, 'patch' ) ).toBe( next ); - } ); - } ); - - describe( 'increment=minor', () => { - const testData = [ - [ '1.0.0', '1.1.0' ], - [ '5.3.9', '5.4.0' ], - [ '0.54.3', '0.55.0' ], - [ '1.0.0-beta.1', '1.0.0' ], - [ '999.9.9-rc.1', '999.10.0' ], - ]; - it.each( testData )( 'Should increment %s to %s', ( current, next ) => { - expect( getNextVersion( current, 'minor' ) ).toBe( next ); - } ); - } ); - - describe( 'increment=major', () => { - const testData = [ - [ '1.0.0', '2.0.0' ], - [ '1.15.999', '2.0.0' ], - [ '5.3.9', '6.0.0' ], - [ '0.54.3', '1.0.0' ], - [ '1.0.0-beta.1', '1.0.0' ], - [ '999.9.9-rc.1', '1000.0.0' ], - ]; - it.each( testData )( 'Should increment %s to %s', ( current, next ) => { - expect( getNextVersion( current, 'major' ) ).toBe( next ); - } ); - } ); - - describe( 'increment=prerelease preid=beta', () => { - const testData = [ - [ '1.0.0', '1.0.1-beta.1' ], - [ '1.0.0-beta.1', '1.0.0-beta.2' ], - [ '3.2.5', '3.2.6-beta.1' ], - ]; - it.each( testData )( 'Should increment %s to %s', ( current, next ) => { - expect( getNextVersion( current, 'prerelease', 'beta' ) ).toBe( next ); - } ); - } ); -} ); diff --git a/packages/dev/test/utils/get-project-privacy.test.js b/packages/dev/test/utils/get-project-privacy.test.js deleted file mode 100644 index 2159421a16..0000000000 --- a/packages/dev/test/utils/get-project-privacy.test.js +++ /dev/null @@ -1,69 +0,0 @@ -jest.mock( '../../src/utils/get-project-slug' ); - -// eslint-disable-next-line camelcase -const childProcess = require( 'child_process' ), - getProjectSlug = require( '../../src/utils/get-project-slug' ), - { getProjectPrivacy, isProjectPublic, isProjectPrivate } = require( '../../src/utils/get-project-privacy' ); - -// Mocked return values. -let mockedSlug, - mockedApiReturn; - -jest.mock( 'child_process' ); -childProcess.execSync.mockImplementation( () => mockedApiReturn ); - -getProjectSlug.mockImplementation( () => mockedSlug ); - -/** - * Mock the API return retrieved by `gh api...`. - * - * @since [version] - * - * @param {boolean} isPublic Whether or not the repo is public. Pass `undefined` to for an "unknown" return. - * @return {string|Object} A JSON string or object to be parsed. Returning an object causes an error for the test unknown responses. - */ -function getMockApiReturn( isPublic ) { - if ( undefined === isPublic ) { - return {}; // Causes an error which is enough to get the proper return. - } - return JSON.stringify( { private: ! isPublic } ); -} - -describe( 'getProjectPrivacy', () => { - const testData = [ - [ 'Should return "public" for public repos', 'lifterlms', 'public', true ], - [ 'Should return "private" for private repos', 'lifterlms-groups', 'private', false ], - [ 'Should return "unknown" for invalid repos', 'fake-repo', 'unknown', undefined ], - ]; - it.each( testData )( '%s', ( name, slug, expected, isPublic ) => { - mockedApiReturn = getMockApiReturn( isPublic ); - mockedSlug = slug; - expect( getProjectPrivacy() ).toBe( expected ); - } ); -} ); - -describe( 'isProjectPublic', () => { - const testData = [ - [ 'Should return true for public repos', 'lifterlms', true ], - [ 'Should return false for private repos', 'lifterlms-groups', false ], - [ 'Should return undefined for invalid repos', 'fake-repo', undefined ], - ]; - it.each( testData )( '%s', ( name, slug, expected ) => { - mockedApiReturn = getMockApiReturn( expected ); - mockedSlug = slug; - expect( isProjectPublic() ).toBe( expected ); - } ); -} ); - -describe( 'isProjectPrivate', () => { - const testData = [ - [ 'Should return false for public repos', 'lifterlms', false, true ], - [ 'Should return true for private repos', 'lifterlms-groups', true, false ], - [ 'Should return undefined for invalid repos', 'fake-repo', undefined, undefined ], - ]; - it.each( testData )( '%s', ( name, slug, expected, isPublic ) => { - mockedApiReturn = getMockApiReturn( isPublic ); - mockedSlug = slug; - expect( isProjectPrivate() ).toBe( expected ); - } ); -} ); diff --git a/packages/dev/test/utils/get-project-slug.test.js b/packages/dev/test/utils/get-project-slug.test.js deleted file mode 100644 index 0d464085d6..0000000000 --- a/packages/dev/test/utils/get-project-slug.test.js +++ /dev/null @@ -1,7 +0,0 @@ -const { getProjectSlug } = require( '../../src/utils' ); - -describe( 'getProjectSlug', () => { - it( 'should return the directory name of the project', () => { - expect( getProjectSlug() ).toBe( 'lifterlms' ); - } ); -} ); diff --git a/packages/dev/test/utils/parse-changelog-file.test.js b/packages/dev/test/utils/parse-changelog-file.test.js deleted file mode 100644 index 085c11c627..0000000000 --- a/packages/dev/test/utils/parse-changelog-file.test.js +++ /dev/null @@ -1,20 +0,0 @@ -const - semver = require( 'semver' ), - { parseChangelogFile } = require( '../../src/utils' ); - -describe( 'parseChangelogFile', () => { - it( 'should parse the changelog file', () => { - const parsed = parseChangelogFile( process.cwd() + '/CHANGELOG.md' ); - - parsed.forEach( ( { date, version, logs } ) => { - // Valid version. - expect( semver.valid( version ) ).toStrictEqual( version ); - - // Valid date. - expect( Date.parse( date ) ).not.toBeNaN(); - - // Should be a string. - expect( typeof logs ).toStrictEqual( 'string' ); - } ); - } ); -} ); diff --git a/packages/dev/test/utils/parse-issue-string.test.js b/packages/dev/test/utils/parse-issue-string.test.js deleted file mode 100644 index f6669e0127..0000000000 --- a/packages/dev/test/utils/parse-issue-string.test.js +++ /dev/null @@ -1,12 +0,0 @@ -const { parseIssueString } = require( '../../src/utils' ); - -describe( 'parseIssueString', () => { - const testData = [ - [ 'Should accept issue references to the current project', { org: 'gocodebox', repo: 'lifterlms', num: '123' }, '#123' ], - [ 'Should accept issue references to the current project', { org: 'org', repo: 'repo', num: '456' }, 'org/repo#456' ], - ]; - it.each( testData )( '%s', ( name, expected, issue ) => { - expect( parseIssueString( issue ) ).toStrictEqual( expected ); - } ); -} ); - diff --git a/packages/dev/test/utils/repo-links.test.js b/packages/dev/test/utils/repo-links.test.js deleted file mode 100644 index c1d1d2a979..0000000000 --- a/packages/dev/test/utils/repo-links.test.js +++ /dev/null @@ -1,50 +0,0 @@ -jest.mock( '../../src/utils/get-project-slug' ); - -const { getFileLink, getRepoLink, getIssueLink } = require( '../../src/utils' ), - getProjectSlug = require( '../../src/utils/get-project-slug' ); - -let mockedSlug = ''; -getProjectSlug.mockImplementation( () => mockedSlug ? mockedSlug : 'lifterlms' ); - -describe( 'repoLinks', () => { - beforeEach( () => { - mockedSlug = 'lifterlms'; - } ); - - describe( 'getFileLink', () => { - const path = 'inc/file.php', - testData = [ - [ 'Should use trunk if a branch is not specified', 'https://github.com/gocodebox/lifterlms/blob/trunk/inc/file.php' ], - [ 'Should use the specified branch', 'https://github.com/gocodebox/lifterlms/blob/dev-123/inc/file.php', 'dev-123' ], - [ 'Should use the specified version tag', 'https://github.com/gocodebox/lifterlms/blob/1.0.0/inc/file.php', '1.0.0' ], - [ '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' ], - ]; - it.each( testData )( '%s', ( name, expected, branch = undefined ) => { - expect( getFileLink( path, branch ) ).toBe( expected ); - } ); - } ); - - describe( 'getIssueLink', () => { - const testData = [ - [ 'Should accept issue references to the current project', 'https://github.com/gocodebox/lifterlms/issues/123', '#123' ], - [ 'Should accept issue references to the another project', 'https://github.com/org/repo/issues/456', 'org/repo#456' ], - ]; - it.each( testData )( '%s', ( name, expected, issue ) => { - expect( getIssueLink( issue ) ).toBe( expected ); - } ); - } ); - - describe( 'getRepoLink', () => { - const testData = [ - [ 'Should use the default slug and organization when no values (undefined) are provided', 'https://github.com/gocodebox/lifterlms', undefined, undefined ], - [ 'Should use the default slug and organization when null values are provided', 'https://github.com/gocodebox/lifterlms', null, null ], - [ 'Should use the default slug and organization when empty values are provided', 'https://github.com/gocodebox/lifterlms', '', false ], - [ 'Should use the specified slug and organization when provided', 'https://github.com/org/slug', 'slug', 'org' ], - ]; - it.each( testData )( '%s', ( name, expected, project, org ) => { - mockedSlug = project; - expect( getRepoLink( project, org ) ).toBe( expected ); - } ); - } ); -} ); - diff --git a/packages/dev/test/utils/validate-changelog.test.js b/packages/dev/test/utils/validate-changelog.test.js deleted file mode 100644 index 2d02d78ef3..0000000000 --- a/packages/dev/test/utils/validate-changelog.test.js +++ /dev/null @@ -1,202 +0,0 @@ -const { - isAttributionValid, - isEntryValid, - isLinkValid, - getChangelogValidationIssues, -} = require( '../../src/utils' ); - -describe( 'isAttributionValid', () => { - const testData = [ - // Valid data. - [ 'Should accept a GitHub username', '@username', true ], - [ 'Should accept a markdown link', '[username](https://fake.tld)', true ], - // Invalid data. - [ 'Should not accept a username without a leading @ symbol', 'username', false ], - [ 'Should not accept a markdown link without a fully qualified URL', '[username](www.fake.tld)', false ], - [ 'Should not accept a markdown reference link', '[username][link]', false ], - // Weird types. - [ 'Should not accept an integer', 123, false ], - [ 'Should not accept an object', {}, false ], - [ 'Should not accept an array', [], false ], - ]; - it.each( testData )( '%s', ( name, input, expected ) => { - expect( isAttributionValid( input ) ).toStrictEqual( expected ); - } ); -} ); - -describe( 'isEntryValid', () => { - describe( 'Single-line entries', () => { - const testData = [ - // Valid. - [ 'Should accept any capital letter and full stop at the end: A -> period', 'A valid line.', true ], - [ 'Should accept any capital letter and full stop at the end: T -> period', 'This is also valid.', true ], - [ 'Should accept any capital letter and full stop at the end: Z -> question mark', 'Z This is also valid?', true ], - [ 'Should accept any capital letter and full stop at the end: P -> exclamation point', 'Plus this is too!', true ], - [ 'Contains multiple sentences', 'Are multiple sentences okay? Yes, This is okay.', true ], - // Invalid. - [ 'Should not start with a plus bullet character', '+ The bullet is added automatically so this is invalid.', false ], - [ 'Should not start with a minus bullet character', '- Other types of bullets are also invalid.', false ], - [ 'Should not start with a lowercase letter', 'must be capital letter.', false ], - [ 'Should not end without a full stop character', 'Must end in a fullstop', false ], - [ 'Should not start with a number', '1 is numeric so it\'s invalid!', false ], - [ 'Should not start with leading spaces', ' Leading spaces are invalid.', false ], - [ 'Should not start with leading tabs', ' Leading tabs are invalid.', false ], - [ 'Should not end with trailing new lines', 'Trailing tabs are invalid.\n', false ], - [ 'Should not end with trailing tabs', 'Trailing tabs are invalid. ', false ], - [ 'Should not end with trailing space', 'Trailing spaces are invalid. ', false ], - [ 'Should not end have multiple sentences without a full stop at the end', 'Trailing characters are invalid. Another', false ], - ]; - it.each( testData )( '%s', ( name, input, expected ) => { - expect( isEntryValid( input ) ).toStrictEqual( expected ); - } ); - } ); - describe( 'Multi-line entries', () => { - const testData = [ - // Valid. - [ 'Should accept a multi-line entry with only a single valid line', '+ A single valid line.\n', true ], - [ 'Should accept any number of valid lines', '+ Multiple valid lines?\n+ Of course.\n+ They\'re okay!', true ], - [ 'Should accept nested indented lines and lines ending in a colon', '+ Nested indentations are okay:\n + Yes.\n + Me 2.', true ], - // Invalid. - [ 'Should not allow the minus character to be used as a bullet', '- No minus signs allowed.\n', false ], - [ 'Should not allow indentation greater than one level deep', ' + This has three spaces.\n', false ], - [ 'Should not allow any lines missing a full stop', '+ Not okay\n+ Okay!', false ], - [ 'Should not allow any lines with improper capitalization', '+ capitalization is still important.\n+ Okay!', false ], - ]; - it.each( testData )( '%s', ( name, input, expected ) => { - expect( isEntryValid( input ) ).toStrictEqual( expected ); - } ); - } ); -} ); - -describe( 'isLinkValid', () => { - const testData = [ - // Valid data. - [ 'Should accept a reference to the current repo', '#123', true ], - [ 'Should accept a reference to another repo', 'org/repo#123', true ], - // Invalid data. - [ 'Should not accept a local reference without a # symbol', '123', false ], - [ 'Should not accept a reference to another repo without a # symbol', 'org/repo123', false ], - [ 'Should not accept a reference to another repo without an organization', 'repo#123', false ], - // Weird types. - [ 'Should not accept an integer', 123, false ], - [ 'Should not accept an object', {}, false ], - [ 'Should not accept an array', [], false ], - ]; - it.each( testData )( '%s', ( name, input, expected ) => { - expect( isLinkValid( input ) ).toStrictEqual( expected ); - } ); -} ); - -describe( 'getChangelogValidationIssues', () => { - it( 'should return errors when missing required fields', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( {}, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'Missing required field: "significance".', 'Missing required field: "type".', 'Missing required field: "entry".' ] ); - } ); - - it( 'should return errors for invalid entry values', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { significance: 'patch', type: 'changed', entry: 'invalid' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'The submitted entry text did not pass validation.' ] ); - } ); - - it( 'should return errors for invalid significance values', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { significance: 'fake', type: 'changed', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'Invalid value "fake" supplied for field: "significance".' ] ); - } ); - - it( 'should return errors for invalid type values', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { type: 'fake', significance: 'patch', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'Invalid value "fake" supplied for field: "type".' ] ); - } ); - - it( 'should return warnings when non-standard keys are found in the entry object', () => { - const { valid, warnings } = getChangelogValidationIssues( { extra: 1 }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [ 'Unexpected key: "extra".' ] ); - } ); - - it( 'should return errors when an array is not submitted for the attributions list', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { attributions: 1, type: 'changed', significance: 'patch', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'The "attributions" field must be an array.' ] ); - } ); - - it( 'should return errors when an array is not submitted for the links list', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { links: 1, type: 'changed', significance: 'patch', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'The "links" field must be an array.' ] ); - } ); - - it( 'should return errors when an invalid attribution is submitted', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { attributions: [ 'abc' ], type: 'changed', significance: 'patch', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'The attribution "abc" is invalid.' ] ); - } ); - - it( 'should return errors when an invalid link is submitted', () => { - const { valid, errors, warnings } = getChangelogValidationIssues( { links: [ 'abc' ], type: 'changed', significance: 'patch', entry: 'Valid.' }, false ); - - expect( valid ).toStrictEqual( false ); - expect( warnings ).toStrictEqual( [] ); - expect( errors ).toStrictEqual( [ 'The link "abc" is invalid.' ] ); - } ); - - const testData = [ - [ - 'should validate a valid entry that is missing optional fields', - { - significance: 'major', - type: 'added', - entry: 'Entry content.', - }, - [], - ], - [ - 'should validate a valid entry that includes valid optional fields', - { - significance: 'major', - type: 'added', - entry: 'Entry content.', - comment: 'A comment', - title: 'title', - attributions: [ '@username', '[user](https://fake.tld)' ], - links: [ '#1234', 'org/repo#123' ], - }, - [], - ], - [ - 'should validate a valid entry that has warnings and no errors', - { - significance: 'major', - type: 'added', - entry: 'Entry content.', - fake: 'extra-field-generates-warning', - }, - [ 'Unexpected key: "fake".' ], - ], - ]; - it.each( testData )( '%s', ( name, entry, expectedWarnings ) => { - const { valid, errors, warnings } = getChangelogValidationIssues( entry, false ); - expect( valid ).toStrictEqual( true ); - expect( warnings ).toStrictEqual( expectedWarnings ); - expect( errors ).toStrictEqual( [] ); - } ); -} ); diff --git a/packages/llms-e2e-test-utils/.llmsdev.yml b/packages/llms-e2e-test-utils/.llmsdev.yml deleted file mode 100644 index 66965cbfef..0000000000 --- a/packages/llms-e2e-test-utils/.llmsdev.yml +++ /dev/null @@ -1,2 +0,0 @@ -update-version: - skip-config: true diff --git a/packages/llms-e2e-test-utils/.npmrc b/packages/llms-e2e-test-utils/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/llms-e2e-test-utils/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/llms-e2e-test-utils/CHANGELOG.md b/packages/llms-e2e-test-utils/CHANGELOG.md deleted file mode 100644 index b747b54cdf..0000000000 --- a/packages/llms-e2e-test-utils/CHANGELOG.md +++ /dev/null @@ -1,86 +0,0 @@ -LifterLMS E2E Test Utils Changelog -================================== - -v3.2.0 - 2022-01-31 -------------------- - -+ Added: New function `wpVersionCompare()` used to run version comparisons against the currently tested version of WordPress. -+ Fixed: Tests failing when running `runSetupWizard()` on WordPress >= 5.9. -+ 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). - - -v3.1.0 - 2021-12-07 -------------------- - -+ Added new functions `highlightNode()` and `setCheckboxSetting()`. - - -v3.0.0 - 2021-11-05 -------------------- - -+ **[Breaking]** Minimum required Puppeteer version raised from 3.0.0 to 5.3.0. -+ Use `waitForTimeout()` in favor of deprecated `waitFor()`. -+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option. - - -v3.0.0-beta.1 - 2021-09-10 --------------------------- - -+ **[Breaking]** Minimum required Puppeteer version raised from 3.0.0 to 5.3.0. -+ Use `waitForTimeout()` in favor of deprecated `waitFor()`. -+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option. - - -v2.3.1 - 2021-10-05 -------------------- - -+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option. - - -v2.3.0 - 2021-06-22 -------------------- - -+ Bugfix: Focus on the search selector prior to typing in select2 search fields. - - -v2.2.2 - 2021-02-04 -------------------- - -+ `click()` now always uses `waitForSelector()`. before clicking the element. -+ Use `waitForSelector()` in favor of `waitFor()` when creating access plans. - - -v2.2.1 - 2021-01-19 -------------------- - -+ Options object is now optional for the createUser() function. -+ Added `args.voucher` to enable voucher usage during registration via the registerStudent() function. - - -v2.2.0 - 2020-11-16 -------------------- - -+ `createCourse()` now uses generic `createPost()` for course creation. -+ `createUser()` now returns the WP_User ID in the return object. -+ `importCourse()` has been updated to accommodate changes in LifterLMS core version 4.8.0. -+ `runSetupWizard()` has been updated to accommodate setup wizard changes in LifterLMS core version 4.8.0. - - -v2.1.3 - 2020-08-06 -------------------- -+ `logoutUser()`: Wait 1 second before navigating to logout page. -+ `visitSettingsPage()`: Don't add null values to the query string. - -v2.1.1 - 2020-08-06 -------------------- - -+ `createCourse()` now uses `createPost()`. -+ `createUser()` will now return the `WP_User` ID of the created user. - -+ Added new utility functions: - - + `createMembership()`: Create and publish a new membership. - + `createPost()`: Create a publish a new post (of a defined post type). - + `enrollStudent()`: Enroll a user account into a course or membership. - + `importCourse()`: Import a course export file into the test environment. - + `setSelect2Option()`: Set the value of a select field powered by select2.js diff --git a/packages/llms-e2e-test-utils/README.md b/packages/llms-e2e-test-utils/README.md deleted file mode 100644 index 574bf91650..0000000000 --- a/packages/llms-e2e-test-utils/README.md +++ /dev/null @@ -1,422 +0,0 @@ -# LifterLMS E2E Test Utilities - -End-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. - -## Installation - -Install the module - -```bash -npm install --save-dev @lifterlms/llms-e2e-test-utils` -``` - -## Changelog - -[View the Changelog](./CHANGELOG.md) - -## API Docs - -<!-- START TOKEN(Autogenerated API docs) --> - -### click - -Click an elements by selector - -_Parameters_ - -- _selector_ `string`: Element selector string. - -_Returns_ - -- `void`: - -### clickAndWait - -Click an element and wait for navigation. - -_Parameters_ - -- _selector_ `string`: Query selector for the DOM element to click. -- _waitUntil_ `string`: Network connection to wait for, defaults to 'networkidle2'. - -_Returns_ - -- `void`: - -### clickElementByText - -Click an element by Text - -_Parameters_ - -- _string_ `string`: Case-insensitive string to search. -- _selector_ `string`: Selector to search. Default "\*". - -_Returns_ - -- `void`: - -### createAccessPlan - -Create and publish a new course - -_Parameters_ - -- _args_ `Object`: Creation arguments. -- _args.postId_ `number`: Post ID of the plan's course or membership. -- _args.price_ `number`: Plan price. -- _args.title_ `string`: Plan title. - -_Returns_ - -- `string`: The created plan's purchase link URL. - -### createCertificate - -Create and publish a new certificate - -_Type_ - -- `number`certificateId WP Post ID of the created certificate post. -- `number`engagementId WP Post ID of the created engagement post. } - -_Parameters_ - -- _args_ `Object`: Optional creation arguments. -- _args.title_ `string`: Certificate title. -- _args.content_ `string`: HTML content of the certificate. -- _args.adminTitle_ `string`: Admin title. -- _args.engagement_ `string`: If supplied, also creates an engagement trigger. This should be the ID of a trigger - -_Returns_ - -- `Object`: { Object containing information about the created post(s). - -### createCoupon - -Create and publish a new course - -_Parameters_ - -- _args_ `Object`: Creation arguments. -- _args.code_ `string`: Coupon code (post title). -- _args.discount_ `string`: The discount amount with either a leading `$` to specify dollar amount discounts or a trailing `%` for percentage discounts. - -_Returns_ - -- `string`: The coupon code. - -### createCourse - -Create and publish a new course - -_Parameters_ - -- _title_ `string`: Course title. - -_Returns_ - -- `number`: The created course's WP_Post ID. - -### createEngagement - -Create and publish a new certificate - -_Parameters_ - -- _engagementId_ `number`: WP_Post ID of the a certificate, email, or achievement post. -- _args_ `Object`: Optional creation arguments. -- _args.title_ `string`: Engagement title. -- _args.trigger_ `string`: ID of the engagement trigger event. -- _args.type_ `string`: Engagement type: certificate, email, or achievement. -- _args.delay_ `number`: Engagement delay, in days. - -_Returns_ - -- `number`: WP Post ID of the created certificate post. - -### createMembership - -Create and publish a new membership - -_Parameters_ - -- _title_ `string`: Membership title. - -_Returns_ - -- `number`: The created membership's WP_Post ID. - -### createPost - -Create and publish a new post - -_Parameters_ - -- _postType_ `string`: WP_Post type. -- _title_ `string`: Post title. - -_Returns_ - -- `number`: The created post's WP_Post ID. - -### createUser - -Create a new user. - -_Parameters_ - -- _opts_ `Object`: Hash of user information used to create the new user. - -_Returns_ - -- `Object`: Object of created user data. - -### createVoucher - -Create and publish a new course - -_Parameters_ - -- _args_ `Object`: Creation arguments. -- _args.name_ `string`: Voucher (post) title. -- _args.course_ `string`: Name of a course to add to the voucher. -- _args.membership_ `string`: Name of a membership to add to the voucher. -- _args.codes_ `number`: Number of codes to generate. -- _args.uses_ `number`: Number of uses per code. - -_Returns_ - -- `string[]`: Array of the generated voucher codes. - -### dismissEditorWelcomeGuide - -Dismiss the "Welcome Guide" in the block editor (if it's active) - -_Returns_ - -- `void`: - -### enrollStudent - -Enroll a student into a course - -This performs as "manual" enrollment using the enrollment -area on the course or membership. - -_Parameters_ - -- _postId_ `number`: WP_Post ID. -- _studentId_ `number`: WP_User ID. - -_Returns_ - -- `void`: - -### fillField - -Type text into a field identified by a selector. - -_Parameters_ - -- _selector_ `string`: Query selector to identify the field element. -- _text_ `string`: Text to type into the field. - -_Returns_ - -- `void`: - -### findElementByText - -Find an element by Text - -_Related_ - -- - -_Parameters_ - -- _string_ `string`: Case-insensitive string to search. -- _selector_ `string`: Selector to search. Default "\*". - -_Returns_ - -- `Array`: Element. - -### highlightNode - -Highlight (selects) the contents of a node. - -_Parameters_ - -- _selector_ `string`: Query selector. -- _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. - -_Returns_ - -- `boolean|string`: Returns the copied text or `true` if `copySelection` is `false`. - -### importCourse - -Import a course JSON file - -_Parameters_ - -- _importFile_ `string`: Filename of the import. -- _importPath_ `string`: Local path where the file is located. By default uses `tests/assets/`. -- _navigate_ `boolean`: Whether or not to automatically navigate to the imported course when done. - -_Returns_ - -- `void`: - -### loginStudent - -Login a user via the LifterLMS student dashboard. - -_Parameters_ - -- _login_ `string`: User login or email address. -- _pass_ `string`: User password. - -_Returns_ - -- `void`: - -### logoutUser - -Logout the current user. - -_Returns_ - -- `void`: - -### registerStudent - -Register a new student via the LifterLMS Open Registration Page - -_Type_ - -- `string`email User's email address. -- `string`pass User's password. } - -_Parameters_ - -- _args_ `Object`: Function arguments object. -- _args.email_ `string`: Email address. If not supplied one will be created from the first name and last name. -- _args.pass_ `string`: User password. If not supplied one will be automatically generated. -- _args.first_ `string`: User's first name. -- _args.last_ `string`: User's last name. -- _args.voucher_ `string`: Voucher code to use during registration. -- _args.address1_ `string`: User's address line 1. -- _args.address2_ `string`: User's address line 2. -- _args.city_ `string`: User's city. -- _args.country_ `string`: User's country. -- _args.state_ `string`: User's state. -- _args.postcode_ `string`: User's postcode. -- _args.phone_ `string`: User's phone. - -_Returns_ - -- `Object`: { Object containing information about the newly created user. - -### runSetupWizard - -Run (and test) the LifterLMS Setup Wizard - -_Parameters_ - -- _options_ `Object`: Options object. -- _options.coursesToImport_ `string[]`: Titles of the course(s) to import through the setup wizard. Pass a falsy to skip import and "Start from Scratch". -- _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.\` - -_Returns_ - -- `void`: - -### select2Select - -Select a value from a select2 dropdown field - -_Parameters_ - -- _selector_ `string`: Query selector for the select element. -- _value_ `string`: Option value to select. - -_Returns_ - -- `void`: - -### setCheckboxSetting - -Toggles a LifterLMS checkbox setting. - -_Parameters_ - -- _selector_ `string`: Selector for the setting checkbox. -- _status_ `boolean`: Requested setting status. Use `true` for checked and `false` for unchecked. -- _save_ `boolean`: Whether or not to perform a save after updating the setting. - -_Returns_ - -- `void`: - -### setSelect2Option - -Set the value of a select2 dropdown field - -This does not actually test whether or not select2 is working, -instead it selects the value on the select element and artificially -triggers a change event. - -_Parameters_ - -- _selector_ `string`: Query selector for the select element. -- _value_ `string`: Option value to select. -- _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. - -_Returns_ - -- `void`: - -### toggleOpenRegistration - -Toggles the open registration setting on or off - -_Parameters_ - -- _status_ `boolean`: Whether to toggle on (`true`) or off (`false`). - -_Returns_ - -- `void`: - -### visitPage - -Visits a page on the WordPress site. - -_Parameters_ - -- _path_ `string`: URL path. Eg: "dashboard" to visit mysite.com/dashboard. -- _query_ `string`: Query string to be added to the url. Eg: "myvar=1&anothervar=2". - -_Returns_ - -- `void`: - -### visitSettingsPage - -Visit a LifterLMS Settings Page on the admin panel - -_Parameters_ - -- _args_ `Object`: Arguments object. -- _args.tab_ `string`: Settings page tab ID. -- _args.section_ `string`: Settings page section ID. - -_Returns_ - -- `void`: - - -<!-- END TOKEN(Autogenerated API docs) --> diff --git a/packages/llms-e2e-test-utils/package.json b/packages/llms-e2e-test-utils/package.json deleted file mode 100644 index 0ebe8d7797..0000000000 --- a/packages/llms-e2e-test-utils/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@lifterlms/llms-e2e-test-utils", - "version": "3.2.0", - "description": "E2E testing utilities for LifterLMS projects.", - "author": "Team LifterLMS <dev@lifterlms.com>", - "license": "GPL-3.0-or-later", - "homepage": "https://github.com/gocodebox/lifterlms/tree/master/packages/llms-e2e-test-utils", - "keywords": [ - "lifterlms", - "wordpress", - "e2e", - "utils" - ], - "repository": { - "type": "git", - "url": "https://github.com/gocodebox/lifterlms.git", - "directory": "packages/llms-e2e-test-utils" - }, - "bugs": { - "url": "https://github.com/gocodebox/lifterlms/labels/package%3A%20llms-e2e-test-utils" - }, - "main": "src/index.js", - "dependencies": { - "@wordpress/e2e-test-utils": "^6.0.0", - "css-xpath": "^1.0.0", - "semver": "^7.3.5" - }, - "peerDependencies": { - "lodash": "^4.17.21" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "docgen": "docgen src/index.js --output README.md --to-token", - "dev": "./../dev/src/index.js" - } -} diff --git a/packages/llms-e2e-test-utils/src/.eslintrc.js b/packages/llms-e2e-test-utils/src/.eslintrc.js deleted file mode 100644 index ad2fbed27e..0000000000 --- a/packages/llms-e2e-test-utils/src/.eslintrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - globals: { - Event: true, // Native JS. - expect: true, // Node native. - page: true, // From Jest. - jQuery: true, // WP Core. - }, -}; diff --git a/packages/llms-e2e-test-utils/src/click-and-wait.js b/packages/llms-e2e-test-utils/src/click-and-wait.js deleted file mode 100644 index 568ef6e2a4..0000000000 --- a/packages/llms-e2e-test-utils/src/click-and-wait.js +++ /dev/null @@ -1,18 +0,0 @@ -import { click } from './click'; - -/** - * Click an element and wait for navigation. - * - * @since 3.37.8 - * - * @param {string} selector Query selector for the DOM element to click. - * @param {string} waitUntil Network connection to wait for, defaults to 'networkidle2'. - * @return {void} - */ -export async function clickAndWait( selector, waitUntil ) { - waitUntil = waitUntil || 'networkidle2'; - await Promise.all( [ - click( selector ), - page.waitForNavigation( { waitUntil } ), - ] ); -} diff --git a/packages/llms-e2e-test-utils/src/click-element-by-text.js b/packages/llms-e2e-test-utils/src/click-element-by-text.js deleted file mode 100644 index 6345e690b9..0000000000 --- a/packages/llms-e2e-test-utils/src/click-element-by-text.js +++ /dev/null @@ -1,15 +0,0 @@ -const { findElementByText } = require( './find-element-by-text' ); - -/** - * Click an element by Text - * - * @since 2.2.0 - * - * @param {string} string Case-insensitive string to search. - * @param {string} selector Selector to search. Default "*". - * @return {void} - */ -export async function clickElementByText( string, selector = '*' ) { - const el = await findElementByText( string, selector ); - await el.click(); -} diff --git a/packages/llms-e2e-test-utils/src/click.js b/packages/llms-e2e-test-utils/src/click.js deleted file mode 100644 index 14d21aaf00..0000000000 --- a/packages/llms-e2e-test-utils/src/click.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Click an elements by selector - * - * @since 2.0.0 - * - * @since 2.2.2 Always waitForSelector before clicking the element. - * @param {string} selector Element selector string. - * @return {void} - */ -export async function click( selector ) { - await page.waitForSelector( selector ); - await page.$eval( selector, ( el ) => el.click() ); -} diff --git a/packages/llms-e2e-test-utils/src/create-access-plan.js b/packages/llms-e2e-test-utils/src/create-access-plan.js deleted file mode 100644 index a551f63581..0000000000 --- a/packages/llms-e2e-test-utils/src/create-access-plan.js +++ /dev/null @@ -1,58 +0,0 @@ -import { click } from './click'; -import { clickAndWait } from './click-and-wait'; -import { createCourse } from './create-course'; -import { fillField } from './fill-field'; - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new course - * - * @since 2.0.0 - * @since 2.2.2 Use `waitForSelector()`` in favor of `waitFor()`. - * - * @param {Object} args Creation arguments. - * @param {number} args.postId Post ID of the plan's course or membership. - * @param {number} args.price Plan price. - * @param {string} args.title Plan title. - * @return {string} The created plan's purchase link URL. - */ -export async function createAccessPlan( { - postId = null, - price = 0.0, - title = 'Test Plan', -} ) { - postId = postId || ( await createCourse() ); - - await visitAdminPage( 'post.php', `post=${ postId }&action=edit` ); - - await click( '#llms-new-access-plan' ); - - const selector = '#llms-access-plans .llms-access-plan'; - await page.waitForSelector( selector ); - - await fillField( `${ selector }:last-child input.llms-plan-title`, title ); - - if ( price > 0 ) { - await fillField( - `${ selector }:last-child input.llms-plan-price`, - price - ); - } else { - await click( - `${ selector }:last-child input[type="checkbox"][data-controller-id="llms-plan-is-free"]` - ); - } - - await clickAndWait( '#llms-save-access-plans' ); - - await page.waitForSelector( - `${ selector }:nth-last-child(2) .llms-plan-link`, - { hidden: true } - ); - - return await page.$eval( - `${ selector }:nth-last-child(2) .llms-plan-link a`, - ( el ) => el.href - ); -} diff --git a/packages/llms-e2e-test-utils/src/create-certificate.js b/packages/llms-e2e-test-utils/src/create-certificate.js deleted file mode 100644 index 2ace0b4357..0000000000 --- a/packages/llms-e2e-test-utils/src/create-certificate.js +++ /dev/null @@ -1,69 +0,0 @@ -import url from 'url'; // eslint-disable-line no-unused-vars -import { click } from './click'; -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; -import { createEngagement } from './create-engagement'; -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new certificate - * - * @since 2.1.2 - * - * @param {Object} args Optional creation arguments. - * @param {string} args.title Certificate title. - * @param {string} args.content HTML content of the certificate. - * @param {string} args.adminTitle Admin title. - * @param {string} args.engagement If supplied, also creates an engagement trigger. This should be the ID of a trigger - * @return {Object} { - * Object containing information about the created post(s). - * @type {number} certificateId WP Post ID of the created certificate post. - * @type {number} engagementId WP Post ID of the created engagement post. - * } - */ -export async function createCertificate( { - title = 'Test Certificate', - content = null, - adminTitle = null, - engagement = '', -} = {} ) { - let engagementId; - - adminTitle = adminTitle || `${ title } Admin Title`; - content = - content || - '<p style="text-align: center;"><em>Awarded to</em></p><p style="text-align: center;">{first_name} {last_name}</p><p style="text-align: center;">on {current_date}</p>'; - - await visitAdminPage( - 'post-new.php', - `post_type=llms_certificate&post_title=${ adminTitle }` - ); - - await click( '#content-html' ); - await fillField( '#content', content ); - - await fillField( '#_llms_certificate_title', title ); - - await clickAndWait( '#publish' ); - - const certUrl = await page.url(), - urlObj = new URL( certUrl ); - - const certificateId = urlObj.searchParams.get( 'post' ); - - if ( engagement ) { - engagementId = await createEngagement( certificateId, { - trigger: engagement, - type: 'certificate', - title: `Engagement for ${ title } (ID #${ certificateId })`, - } ); - - // Return to the certificate. - await page.goto( certUrl ); - } - - return { - certificateId, - engagementId, - }; -} diff --git a/packages/llms-e2e-test-utils/src/create-coupon.js b/packages/llms-e2e-test-utils/src/create-coupon.js deleted file mode 100644 index 93101398c8..0000000000 --- a/packages/llms-e2e-test-utils/src/create-coupon.js +++ /dev/null @@ -1,37 +0,0 @@ -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new course - * - * @since 2.0.0 - * - * @param {Object} args Creation arguments. - * @param {string} args.code Coupon code (post title). - * @param {string} args.discount The discount amount with either a leading `$` to specify dollar amount discounts or a trailing `%` for percentage discounts. - * @return {string} The coupon code. - */ -export async function createCoupon( { code = null, discount = '10%' } ) { - code = code || Math.random().toString( 36 ).slice( 2 ); - - await visitAdminPage( - 'post-new.php', - `post_type=llms_coupon&post_title=${ code }` - ); - - await page.select( - '#_llms_discount_type', - discount.includes( '%' ) ? 'percent' : 'dollar' - ); - - await fillField( - '#_llms_coupon_amount', - discount.replace( '%', '' ).replace( '$', '' ) - ); - - await clickAndWait( '#publish' ); - - return code; -} diff --git a/packages/llms-e2e-test-utils/src/create-course.js b/packages/llms-e2e-test-utils/src/create-course.js deleted file mode 100644 index bd55756b68..0000000000 --- a/packages/llms-e2e-test-utils/src/create-course.js +++ /dev/null @@ -1,14 +0,0 @@ -import { createPost } from './create-post'; - -/** - * Create and publish a new course - * - * @since Unknown - * @since 2.2.0 Use `createPost()`. - * - * @param {string} title Course title. - * @return {number} The created course's WP_Post ID. - */ -export async function createCourse( title = 'Test Course' ) { - return createPost( 'course', title ); -} diff --git a/packages/llms-e2e-test-utils/src/create-engagement.js b/packages/llms-e2e-test-utils/src/create-engagement.js deleted file mode 100644 index c0126e0f6b..0000000000 --- a/packages/llms-e2e-test-utils/src/create-engagement.js +++ /dev/null @@ -1,44 +0,0 @@ -import url from 'url'; // eslint-disable-line no-unused-vars -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; -import { setSelect2Option } from './set-select2-option'; -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new certificate - * - * @since 2.1.2 - * - * @param {number} engagementId WP_Post ID of the a certificate, email, or achievement post. - * @param {Object} args Optional creation arguments. - * @param {string} args.title Engagement title. - * @param {string} args.trigger ID of the engagement trigger event. - * @param {string} args.type Engagement type: certificate, email, or achievement. - * @param {number} args.delay Engagement delay, in days. - * @return {number} WP Post ID of the created certificate post. - */ -export async function createEngagement( - engagementId, - { - title = 'Test Engagement', - trigger = 'user_registration', - type = 'certificate', - delay = 0, - } = {} -) { - await visitAdminPage( - 'post-new.php', - `post_type=llms_engagement&post_title=${ title }` - ); - - await setSelect2Option( '#_llms_trigger_type', trigger ); - await setSelect2Option( '#_llms_engagement_type', type ); - await setSelect2Option( '#_llms_engagement', engagementId.toString() ); - - await fillField( '#_llms_engagement_delay', delay.toString() ); - - await clickAndWait( '#publish' ); - - const currUrl = new URL( await page.url() ); - return currUrl.searchParams.get( 'post' ); -} diff --git a/packages/llms-e2e-test-utils/src/create-membership.js b/packages/llms-e2e-test-utils/src/create-membership.js deleted file mode 100644 index 8961ff054f..0000000000 --- a/packages/llms-e2e-test-utils/src/create-membership.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createPost } from './create-post'; - -/** - * Create and publish a new membership - * - * @since 2.2.0 - * - * @param {string} title Membership title. - * @return {number} The created membership's WP_Post ID. - */ -export async function createMembership( title = 'Test Membership' ) { - return createPost( 'llms_membership', title ); -} diff --git a/packages/llms-e2e-test-utils/src/create-post.js b/packages/llms-e2e-test-utils/src/create-post.js deleted file mode 100644 index 5019a2236e..0000000000 --- a/packages/llms-e2e-test-utils/src/create-post.js +++ /dev/null @@ -1,25 +0,0 @@ -import { createNewPost, publishPost } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new post - * - * @since 2.2.0 - * - * @param {string} postType WP_Post type. - * @param {string} title Post title. - * @return {number} The created post's WP_Post ID. - */ -export async function createPost( postType, title = 'Test Course' ) { - page.on( 'dialog', ( dialog ) => dialog.accept() ); - - await createNewPost( { - title, - postType, - } ); - - await publishPost(); - - return await page.evaluate( () => - wp.data.select( 'core/editor' ).getCurrentPostId() - ); -} diff --git a/packages/llms-e2e-test-utils/src/create-user.js b/packages/llms-e2e-test-utils/src/create-user.js deleted file mode 100644 index 7632c1df29..0000000000 --- a/packages/llms-e2e-test-utils/src/create-user.js +++ /dev/null @@ -1,69 +0,0 @@ -// Internal dependencies. -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; - -// External dependencies. -import url from 'url'; // eslint-disable-line no-unused-vars -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Asynchronously loop through an Object - * - * @since 1.0.0 - * - * @param {Object} obj Object to loop through. - * @param {Function} callback Callback function, will be passed to params `key` and `val`. - * @return {void} - */ -const forEach = async ( obj, callback ) => { - const keys = Object.keys( obj ); - for ( let i = 0; i < keys.length; i++ ) { - await callback( keys[ i ], obj[ keys[ i ] ] ); - } -}; - -/** - * Create a new user. - * - * @since 1.0.0 - * @since 2.2.0 Returns the WP_User ID in the return object. - * @since 2.2.1 Options object is now optional. - * - * @param {Object} opts Hash of user information used to create the new user. - * @return {Object} Object of created user data. - */ -export async function createUser( opts = {} ) { - await visitAdminPage( 'user-new.php' ); - - const login = `mock_${ Math.random().toString( 36 ).slice( 2 ) }`; - opts = Object.assign( - { - user_login: login, - email: `${ login }@mock.tld`, - role: 'student', - password: `${ Math.random() - .toString( 36 ) - .slice( 2 ) }${ Math.random().toString( 36 ).slice( 2 ) }`, - }, - opts - ); - - await forEach( opts, async ( key, val ) => { - if ( 'role' === key ) { - await page.select( '#role', val ); - } else if ( 'password' === key ) { - await page.click( '.wp-generate-pw' ); - await fillField( '#pass1', val ); - } else { - await fillField( `#${ key }`, val ); - } - } ); - - await clickAndWait( '#createusersub' ); - - // Add the user's ID. - const currUrl = new URL( await page.url() ); - opts.id = currUrl.searchParams.get( 'id' ); - - return opts; -} diff --git a/packages/llms-e2e-test-utils/src/create-voucher.js b/packages/llms-e2e-test-utils/src/create-voucher.js deleted file mode 100644 index a75f494b71..0000000000 --- a/packages/llms-e2e-test-utils/src/create-voucher.js +++ /dev/null @@ -1,56 +0,0 @@ -import { click } from './click'; -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; -import { select2Select } from './select2-select'; - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Create and publish a new course - * - * @since 2.2.1 - * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - * - * @param {Object} args Creation arguments. - * @param {string} args.name Voucher (post) title. - * @param {string} args.course Name of a course to add to the voucher. - * @param {string} args.membership Name of a membership to add to the voucher. - * @param {number} args.codes Number of codes to generate. - * @param {number} args.uses Number of uses per code. - * @return {string[]} Array of the generated voucher codes. - */ -export async function createVoucher( { - name = 'A Voucher', - course = 'LifterLMS Quickstart Course', - membership = '', - codes = 5, - uses = 5, -} = {} ) { - await visitAdminPage( - 'post-new.php', - `post_type=llms_voucher&post_title=${ name }` - ); - - if ( course ) { - await select2Select( '#_llms_voucher_courses', course ); - } - - if ( membership ) { - await select2Select( '#_llms_voucher_memberships', membership ); - } - - await fillField( '#llms_voucher_add_quantity', codes ); - await fillField( '#llms_voucher_add_uses', uses ); - - await click( '#llms_voucher_add_codes' ); - - await page.waitForSelector( '#llms_voucher_tbody tr' ); - - await clickAndWait( '#publish' ); - await page.waitForTimeout( 1000 ); // Non-interactive tests aren't publishing without a delay, not sure why. - - return await page.$$eval( - '#llms_voucher_tbody input[name="llms_voucher_code[]"', - ( inputs ) => inputs.map( ( input ) => input.value ) - ); -} diff --git a/packages/llms-e2e-test-utils/src/dismiss-editor-welcome-guide.js b/packages/llms-e2e-test-utils/src/dismiss-editor-welcome-guide.js deleted file mode 100644 index 6a4611716f..0000000000 --- a/packages/llms-e2e-test-utils/src/dismiss-editor-welcome-guide.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Dismiss the "Welcome Guide" in the block editor (if it's active) - * - * @since 2.2.0 - * - * @return {void} - */ -export async function dismissEditorWelcomeGuide() { - const isWelcomeGuideActive = await page.evaluate( () => - wp.data.select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ) - ); - if ( isWelcomeGuideActive ) { - await page.evaluate( () => - wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ) - ); - } -} diff --git a/packages/llms-e2e-test-utils/src/enroll-student.js b/packages/llms-e2e-test-utils/src/enroll-student.js deleted file mode 100644 index 0d72d8ea36..0000000000 --- a/packages/llms-e2e-test-utils/src/enroll-student.js +++ /dev/null @@ -1,30 +0,0 @@ -// Internal dependencies. -import { click } from './click'; -import { setSelect2Option } from './set-select2-option'; - -// External dependencies. -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Enroll a student into a course - * - * This performs as "manual" enrollment using the enrollment - * area on the course or membership. - * - * @since 2.2.0 - * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - * - * @param {number} postId WP_Post ID. - * @param {number} studentId WP_User ID. - * @return {void} - */ -export async function enrollStudent( postId, studentId ) { - await visitAdminPage( 'post.php', `post=${ postId }&action=edit` ); - - await setSelect2Option( '#llms-add-student-select', studentId ); - - await click( '#llms-enroll-students' ); - - // Lazy waiting for ajax save. - await page.waitForTimeout( 2000 ); -} diff --git a/packages/llms-e2e-test-utils/src/fill-field.js b/packages/llms-e2e-test-utils/src/fill-field.js deleted file mode 100644 index f32be0d65c..0000000000 --- a/packages/llms-e2e-test-utils/src/fill-field.js +++ /dev/null @@ -1,17 +0,0 @@ -import { pressKeyWithModifier } from '@wordpress/e2e-test-utils'; - -/** - * Type text into a field identified by a selector. - * - * @since 1.1.1 - * @since 2.0.0 Automatically cast `text` to a string. - * - * @param {string} selector Query selector to identify the field element. - * @param {string} text Text to type into the field. - * @return {void} - */ -export async function fillField( selector, text ) { - await page.focus( selector ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.type( selector, text.toString() ); -} diff --git a/packages/llms-e2e-test-utils/src/find-element-by-text.js b/packages/llms-e2e-test-utils/src/find-element-by-text.js deleted file mode 100644 index 904803c382..0000000000 --- a/packages/llms-e2e-test-utils/src/find-element-by-text.js +++ /dev/null @@ -1,18 +0,0 @@ -const cssXPath = require( 'css-xpath' ); - -/** - * Find an element by Text - * - * @since 2.2.0 - * - * @see {@link https://stackoverflow.com/a/47829000/400568} - * - * @param {string} string Case-insensitive string to search. - * @param {string} selector Selector to search. Default "*". - * @return {Array} Element. - */ -export async function findElementByText( string, selector = '*' ) { - return await page.waitForXPath( - `${ cssXPath( selector ) }[contains(text(), '${ string }')]` - ); -} diff --git a/packages/llms-e2e-test-utils/src/get-wp-version.js b/packages/llms-e2e-test-utils/src/get-wp-version.js deleted file mode 100644 index 9c0dd55318..0000000000 --- a/packages/llms-e2e-test-utils/src/get-wp-version.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Retrieve the WP_VERSION environment variable - * - * When running tests locally this will likely be undefined unless running tests with - * `WP_VERSION=5.7.2 npm run test`. - * - * The WP_VERSION env var is defined during CI tests automatically and this function - * is generally used to determine conditionals based on the WP Core version. - * - * For example: block editor selectors change between WP core version, some features - * aren't available on older versions, etc... - * - * @since 5.0.1 - * - * @return {?string} WordPress version or null if not set. - */ -export function getWPVersion() { - const { WP_VERSION } = process.env; - return WP_VERSION || null; -} diff --git a/packages/llms-e2e-test-utils/src/highlight-node.js b/packages/llms-e2e-test-utils/src/highlight-node.js deleted file mode 100644 index 2712e59b75..0000000000 --- a/packages/llms-e2e-test-utils/src/highlight-node.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Highlight (selects) the contents of a node. - * - * @since 3.1.0 - * - * @param {string} selector Query selector. - * @param {boolean} copySelection If `true`, copies the selected text and returns it. - * The browser clipboard-read permission must be granted in order to read from the clipboard. - * @return {boolean|string} Returns the copied text or `true` if `copySelection` is `false`. - */ -export async function highlightNode( selector, copySelection = false ) { - await page.waitForSelector( selector ); - - await page.evaluate( ( _selector ) => { - const range = document.createRange(), - // eslint-disable-next-line @wordpress/no-global-get-selection - selection = window.getSelection(); - - range.selectNodeContents( document.querySelector( _selector ) ); - - selection.removeAllRanges(); - - selection.addRange( range ); - }, selector ); - - if ( copySelection ) { - await page.bringToFront(); - - return await page.evaluate( () => { - document.execCommand( 'copy' ); - return window.navigator.clipboard.readText(); - } ); - } - - return true; -} diff --git a/packages/llms-e2e-test-utils/src/import-course.js b/packages/llms-e2e-test-utils/src/import-course.js deleted file mode 100644 index e04202fd15..0000000000 --- a/packages/llms-e2e-test-utils/src/import-course.js +++ /dev/null @@ -1,44 +0,0 @@ -// Internal dependencies. -import { clickAndWait } from './click-and-wait'; - -// External dependencies. -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Import a course JSON file - * - * @since 2.2.0 - * @since 2.2.0 Update to accommodate changes in the LifterLMS core. - * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - * - * @param {string} importFile Filename of the import. - * @param {string} importPath Local path where the file is located. By default uses `tests/assets/`. - * @param {boolean} navigate Whether or not to automatically navigate to the imported course when done. - * @return {void} - */ -export async function importCourse( - importFile, - importPath = '', - navigate = true -) { - importPath = importPath || `${ process.cwd() }/tests/assets/`; - - const file = importPath + importFile; - - await visitAdminPage( 'admin.php', 'page=llms-import' ); - - await page.click( 'button.page-title-action' ); - - const inputSelector = 'input[name="llms_import"]'; - await page.waitForSelector( inputSelector ); - const fileUpload = await page.$( inputSelector ); - - fileUpload.uploadFile( file ); - await page.waitForTimeout( 1000 ); - - await clickAndWait( '#llms-import-file-submit' ); - - if ( navigate ) { - await clickAndWait( '.llms-admin-notice.notice-success a' ); - } -} diff --git a/packages/llms-e2e-test-utils/src/index.js b/packages/llms-e2e-test-utils/src/index.js deleted file mode 100644 index 39677692b3..0000000000 --- a/packages/llms-e2e-test-utils/src/index.js +++ /dev/null @@ -1,42 +0,0 @@ -export { click } from './click'; -export { clickAndWait } from './click-and-wait'; -export { clickElementByText } from './click-element-by-text'; - -export { createAccessPlan } from './create-access-plan'; -export { createCertificate } from './create-certificate'; -export { createCoupon } from './create-coupon'; -export { createCourse } from './create-course'; -export { createEngagement } from './create-engagement'; -export { createMembership } from './create-membership'; -export { createPost } from './create-post'; -export { createUser } from './create-user'; -export { createVoucher } from './create-voucher'; - -export { dismissEditorWelcomeGuide } from './dismiss-editor-welcome-guide'; - -export { enrollStudent } from './enroll-student'; - -export { fillField } from './fill-field'; - -export { highlightNode } from './highlight-node'; - -export { importCourse } from './import-course'; - -export { findElementByText } from './find-element-by-text'; - -export { loginStudent } from './login-student'; -export { logoutUser } from './logout-user'; - -export { registerStudent } from './register-student'; -export { runSetupWizard } from './run-setup-wizard'; - -export { select2Select } from './select2-select'; -export { setCheckboxSetting } from './set-checkbox-setting'; -export { setSelect2Option } from './set-select2-option'; - -export { toggleOpenRegistration } from './toggle-open-registration'; - -export { visitPage } from './visit-page'; -export { visitSettingsPage } from './visit-settings-page'; - -export { wpVersionCompare } from './wp-version-compare'; diff --git a/packages/llms-e2e-test-utils/src/login-student.js b/packages/llms-e2e-test-utils/src/login-student.js deleted file mode 100644 index 95c29ae771..0000000000 --- a/packages/llms-e2e-test-utils/src/login-student.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Internal Dependencies. - */ -const { clickAndWait } = require( './click-and-wait' ), - { fillField } = require( './fill-field' ), - { visitPage } = require( './visit-page' ); - -/** - * Login a user via the LifterLMS student dashboard. - * - * @since 3.0.0 - * - * @param {string} login User login or email address. - * @param {string} pass User password. - * @return {void} - */ -export async function loginStudent( login, pass ) { - await visitPage( 'dashboard' ); - - await fillField( '#llms_login', login ); - await fillField( '#llms_password', pass ); - - await clickAndWait( '#llms_login_button' ); -} diff --git a/packages/llms-e2e-test-utils/src/logout-user.js b/packages/llms-e2e-test-utils/src/logout-user.js deleted file mode 100644 index 14ca303ff9..0000000000 --- a/packages/llms-e2e-test-utils/src/logout-user.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * External Dependencies. - */ -const { createURL } = require( '@wordpress/e2e-test-utils' ); - -/** - * Internal Dependencies. - */ -const { clickAndWait } = require( './click-and-wait' ); - -/** - * Logout the current user. - * - * @since 3.37.8 - * @since 2.1.2 Wait 1 second before navigating to logout page. - * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - * - * @return {void} - */ -export async function logoutUser() { - await page.waitForTimeout( 1000 ); - await page.goto( createURL( 'wp-login.php', 'action=logout' ) ); - await clickAndWait( 'a' ); -} diff --git a/packages/llms-e2e-test-utils/src/register-student.js b/packages/llms-e2e-test-utils/src/register-student.js deleted file mode 100644 index 4fac5483d8..0000000000 --- a/packages/llms-e2e-test-utils/src/register-student.js +++ /dev/null @@ -1,107 +0,0 @@ -import { click } from './click'; -import { clickAndWait } from './click-and-wait'; -import { fillField } from './fill-field'; -import { logoutUser } from './logout-user'; -import { select2Select } from './select2-select'; -import { visitPage } from './visit-page'; - -/** - * Register a new student via the LifterLMS Open Registration Page - * - * @since 2.1.2 - * @since 2.2.1 Add `args.voucher` to enable voucher usage during registration. - * @since 5.0.0-alpha.2 Add arguments for address fields. - * - * @param {Object} args Function arguments object. - * @param {string} args.email Email address. If not supplied one will be created from the first name and last name. - * @param {string} args.pass User password. If not supplied one will be automatically generated. - * @param {string} args.first User's first name. - * @param {string} args.last User's last name. - * @param {string} args.voucher Voucher code to use during registration. - * @param {string} args.address1 User's address line 1. - * @param {string} args.address2 User's address line 2. - * @param {string} args.city User's city. - * @param {string} args.country User's country. - * @param {string} args.state User's state. - * @param {string} args.postcode User's postcode. - * @param {string} args.phone User's phone. - * @return {Object} { - * Object containing information about the newly created user. - * - * @type {string} email User's email address. - * @type {string} pass User's password. - * } - */ -export async function registerStudent( { - email = null, - pass = null, - first = 'Jamie', - last = 'Doe', - voucher = '', - address1 = '1 Avenue Street', - address2 = '', - city = 'A City', - country = 'United States', - state = 'Texas', - postcode = '52342', - phone = '', -} = {} ) { - const theInt = Math.floor( Math.random() * ( 99990 - 10000 + 1 ) ) + 10000; - - email = email || `${ first }.${ last }+${ theInt }@e2e-tests.tld`; - pass = - pass || - Math.random().toString( 36 ).slice( 2 ) + - Math.random().toString( 36 ).slice( 2 ); - - await logoutUser(); - await visitPage( 'dashboard' ); - - await fillField( '#email_address', email ); - await fillField( '#email_address_confirm', email ); - await fillField( '#password', pass ); - await fillField( '#password_confirm', pass ); - await fillField( '#first_name', first ); - await fillField( '#last_name', last ); - - if ( address1 ) { - await fillField( '#llms_billing_address_1', address1 ); - } - - if ( address2 ) { - await fillField( '#llms_billing_address_2', address2 ); - } - - if ( city ) { - await fillField( '#llms_billing_city', city ); - } - - if ( country && 'United States' !== country ) { - await select2Select( '#llms_billing_country', country ); - } - - if ( state ) { - await select2Select( '#llms_billing_state', state ); - } - - if ( postcode ) { - await fillField( '#llms_billing_zip', postcode ); - } - - if ( phone ) { - await fillField( '#llms_phone', phone ); - } - - if ( voucher ) { - await click( '#llms-voucher-toggle' ); - await page.waitForSelector( '#llms_voucher' ); - await fillField( '#llms_voucher', voucher ); - } - - await clickAndWait( '#llms_register_person' ); - - return { - email, - pass, - }; -} diff --git a/packages/llms-e2e-test-utils/src/run-setup-wizard.js b/packages/llms-e2e-test-utils/src/run-setup-wizard.js deleted file mode 100644 index 2b9b5ee5c5..0000000000 --- a/packages/llms-e2e-test-utils/src/run-setup-wizard.js +++ /dev/null @@ -1,119 +0,0 @@ -import { clickAndWait } from './click-and-wait'; -import { clickElementByText } from './click-element-by-text'; -import { findElementByText } from './find-element-by-text'; -import { wpVersionCompare } from './wp-version-compare'; -import { dismissEditorWelcomeGuide } from './dismiss-editor-welcome-guide'; - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -/** - * Retrieve the Setup Wizard Page Title. - * - * @since 2.1.0 - * - * @return {string} Content of the title element. - */ -const getTitle = async function() { - return await page.$eval( - '.llms-setup-content > form > h1', - ( txt ) => txt.textContent - ); -}; - -/** - * Run (and test) the LifterLMS Setup Wizard - * - * @since 2.1.0 - * @since 2.2.0 Rework to accommodate setup wizard changes in LifterLMS core. - * @since 3.2.0 Fix title assertion on WordPress >= v5.9. - * - * @param {Object} options Options object. - * @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". - * @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.` - * @return {void} - */ -export async function runSetupWizard( { - coursesToImport = [ 'LifterLMS Quickstart Course' ], - exit = false, -} = {} ) { - // Launch the Setup Wizard. - await visitAdminPage( 'admin.php', 'page=llms-setup' ); - - // Step One. - expect( await getTitle() ).toBe( 'Welcome to LifterLMS!' ); - - // Move to Step Two. - await clickAndWait( '.llms-setup-actions .llms-button-primary' ); - expect( await getTitle() ).toBe( 'Page Setup' ); - - // Move to Step Three. - await clickAndWait( '.llms-setup-actions .llms-button-primary' ); - expect( await getTitle() ).toBe( 'Payments' ); - - // Move to Step Four. - await clickAndWait( '.llms-setup-actions .llms-button-primary' ); - expect( await getTitle() ).toBe( 'Help Improve LifterLMS & Get a Coupon' ); - - // Move to Step Five. - await clickAndWait( '.llms-setup-actions .llms-button-secondary' ); // Skip the coupon. - expect( await getTitle() ).toBe( 'Setup Complete!' ); - - // Import button should be disabled. - expect( - await page.$eval( '#llms-setup-submit', ( el ) => el.disabled ) - ).toBe( true ); - - if ( exit ) { - // Exit the wizard. - - await clickAndWait( '.llms-exit-setup' ); - expect( - await page.url().includes( '/admin.php?page=llms-settings' ) - ).toBe( true ); - } else if ( ! coursesToImport ) { - // Start from scratch. - - await clickAndWait( '.llms-setup-actions .llms-button-secondary' ); - await dismissEditorWelcomeGuide(); - } else if ( coursesToImport ) { - // Import courses. - - // Select specified courses. - for ( const courseTitle of coursesToImport ) { - await clickElementByText( courseTitle, 'h3' ); - } - - await clickAndWait( '.llms-setup-actions .llms-button-primary' ); - - if ( 1 === coursesToImport.length ) { - // Single course imported. - - expect( - await page.$eval( - '.block-editor h1.screen-reader-text', - ( txt ) => txt.textContent - ) - ).toBe( 'Edit Course' ); - - await dismissEditorWelcomeGuide(); - - expect( - await page.$eval( - '.editor-post-title__input', - // On >= WP 5.9, this is an <h1>, earlier is a <textarea>. - ( txt, isTextNode ) => isTextNode ? txt.textContent : txt.value, - wpVersionCompare( '5.9' ) - ) - ).toBe( coursesToImport[ 0 ] ); - } else { - expect( - await page.url().includes( '/edit.php?post_type=course' ) - ).toBe( true ); - - // All courses should be present in the post table list. - for ( const courseTitle of coursesToImport ) { - await findElementByText( courseTitle, '#the-list a.row-title' ); - } - } - } -} diff --git a/packages/llms-e2e-test-utils/src/select2-select.js b/packages/llms-e2e-test-utils/src/select2-select.js deleted file mode 100644 index f3de5b3172..0000000000 --- a/packages/llms-e2e-test-utils/src/select2-select.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Select a value from a select2 dropdown field - * - * @since 2.2.1 - * @since 2.3.0 Focus on the search selector prior to typing. - * @since 2.3.1 Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown - * to close after selecting an option. - * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - * @param {string} selector Query selector for the select element. - * @param {string} value Option value to select. - * @return {void} - */ -export async function select2Select( selector, value ) { - // Wait for select2 to load on the element. - await page.waitForSelector( `${ selector }.select2-hidden-accessible` ); - - await page.$eval( selector, ( el ) => { - jQuery( el ).select2( 'open' ); - } ); - - const SEARCH_SELECTOR = '.select2-search__field'; - await page.waitForSelector( SEARCH_SELECTOR ); - await page.focus( SEARCH_SELECTOR ); - - await page.keyboard.type( value ); - await page.waitForTimeout( 1000 ); - - await page.keyboard.press( 'Enter' ); - - // Wait for the selection box to close. - await page.waitForSelector( - `${ selector } + .select2-container .select2-selection[aria-expanded="false"]` - ); -} diff --git a/packages/llms-e2e-test-utils/src/set-checkbox-setting.js b/packages/llms-e2e-test-utils/src/set-checkbox-setting.js deleted file mode 100644 index d828bd5c15..0000000000 --- a/packages/llms-e2e-test-utils/src/set-checkbox-setting.js +++ /dev/null @@ -1,26 +0,0 @@ -// Internal dependencies. -import { clickAndWait } from './click-and-wait'; - -/** - * Toggles a LifterLMS checkbox setting. - * - * @since 3.1.0 - * - * @param {string} selector Selector for the setting checkbox. - * @param {boolean} status Requested setting status. Use `true` for checked and `false` for unchecked. - * @param {boolean} save Whether or not to perform a save after updating the setting. - * @return {void} - */ -export async function setCheckboxSetting( selector, status = true, save = true ) { - await page.waitForSelector( selector ); - - const currStatus = await page.$eval( selector, ( el ) => el.checked ); - - if ( status !== currStatus ) { - await page.click( selector ); - - if ( save ) { - await clickAndWait( '.llms-save .llms-button-primary' ); - } - } -} diff --git a/packages/llms-e2e-test-utils/src/set-select2-option.js b/packages/llms-e2e-test-utils/src/set-select2-option.js deleted file mode 100644 index ea47d8e3dc..0000000000 --- a/packages/llms-e2e-test-utils/src/set-select2-option.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Set the value of a select2 dropdown field - * - * This does not actually test whether or not select2 is working, - * instead it selects the value on the select element and artificially - * triggers a change event. - * - * @since 2.2.0 - * - * @param {string} selector Query selector for the select element. - * @param {string} value Option value to select. - * @param {boolean} create 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. - * @return {void} - */ -export async function setSelect2Option( selector, value, create = true ) { - await page.$eval( - selector, - ( el, _value, _create ) => { - if ( _create ) { - jQuery( el ).append( - '<option value="' + _value + '">' + _value + '</option>' - ); - } - el.value = _value.toString(); - el.dispatchEvent( new Event( 'change' ) ); - }, - value, - create - ); -} diff --git a/packages/llms-e2e-test-utils/src/toggle-open-registration.js b/packages/llms-e2e-test-utils/src/toggle-open-registration.js deleted file mode 100644 index 91d3cc36b4..0000000000 --- a/packages/llms-e2e-test-utils/src/toggle-open-registration.js +++ /dev/null @@ -1,24 +0,0 @@ -import { clickAndWait } from './click-and-wait'; -import { visitSettingsPage } from './visit-settings-page'; - -/** - * Toggles the open registration setting on or off - * - * @since 2.1.2 - * - * @param {boolean} status Whether to toggle on (`true`) or off (`false`). - * @return {void} - */ -export async function toggleOpenRegistration( status ) { - await visitSettingsPage( { tab: 'account' } ); - - const currStatus = await page.$eval( - '#lifterlms_enable_myaccount_registration', - ( el ) => el.checked - ); - - if ( ( status && ! currStatus ) || ( ! status && currStatus ) ) { - await page.click( '#lifterlms_enable_myaccount_registration' ); - await clickAndWait( '.llms-save .llms-button-primary' ); - } -} diff --git a/packages/llms-e2e-test-utils/src/visit-page.js b/packages/llms-e2e-test-utils/src/visit-page.js deleted file mode 100644 index 7cd6b53c87..0000000000 --- a/packages/llms-e2e-test-utils/src/visit-page.js +++ /dev/null @@ -1,13 +0,0 @@ -const { createURL } = require( '@wordpress/e2e-test-utils' ); - -/** - * Visits a page on the WordPress site. - * - * @since 3.37.8 - * @param {string} path URL path. Eg: "dashboard" to visit mysite.com/dashboard. - * @param {string} query Query string to be added to the url. Eg: "myvar=1&anothervar=2". - * @return {void} - */ -export async function visitPage( path, query ) { - await page.goto( createURL( path, query ) ); -} diff --git a/packages/llms-e2e-test-utils/src/visit-settings-page.js b/packages/llms-e2e-test-utils/src/visit-settings-page.js deleted file mode 100644 index b186282613..0000000000 --- a/packages/llms-e2e-test-utils/src/visit-settings-page.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External Dependencies. - */ -import { visitAdminPage } from '@wordpress/e2e-test-utils'; -import { pickBy } from 'lodash'; - -/** - * Visit a LifterLMS Settings Page on the admin panel - * - * @since 2.1.0 - * @since 2.1.2 Don't add null values to the query string. - * - * @param {Object} args Arguments object. - * @param {string} args.tab Settings page tab ID. - * @param {string} args.section Settings page section ID. - * @return {void} - */ -export async function visitSettingsPage( { tab = null, section = null } = {} ) { - await visitAdminPage( - 'admin.php', - new URLSearchParams( - pickBy( { page: 'llms-settings', tab, section } ) - ).toString() - ); -} diff --git a/packages/llms-e2e-test-utils/src/wp-version-compare.js b/packages/llms-e2e-test-utils/src/wp-version-compare.js deleted file mode 100644 index 34ec537d07..0000000000 --- a/packages/llms-e2e-test-utils/src/wp-version-compare.js +++ /dev/null @@ -1,16 +0,0 @@ -import { cmp, coerce } from 'semver'; - -import { getWPVersion } from './get-wp-version'; - -/** - * Run a version compare against the currently tested version of WordPress. - * - * @since 3.2.0 - * - * @param {string} version A version string. - * @param {string} comparator A comparison string, eg ">=" or "<", etc... - * @return {boolean} Comparison result. - */ -export function wpVersionCompare( version, comparator = '>=' ) { - return cmp( coerce( version ), comparator, coerce( getWPVersion() ) ); -} diff --git a/packages/scripts/.npmrc b/packages/scripts/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/packages/scripts/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md deleted file mode 100644 index 20a5b9d5db..0000000000 --- a/packages/scripts/CHANGELOG.md +++ /dev/null @@ -1,78 +0,0 @@ -@lifterlms/scripts CHANGELOG -============================ - -v2.2.0 - 2022-01-31 -------------------- - -+ Update: `@wordpress/scripts` to [20.0.2](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md#2002-2022-01-31). -+ Update: `@jest/test-sequencer` to [27.4.6](https://github.com/facebook/jest/releases/tag/v27.4.6). -+ 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`. - - -v2.1.0 - 2021-12-13 -------------------- - -+ Added webpack configuration option to customize the `cleanAfterEveryBuildPatterns` setting of the `CleanWebpackPlugin`. - - -v2.0.0 - 2021-11-05 -------------------- - -+ **[Breaking]** Raised the minimum required `@wordpress/scripts` version to 18.1.0. -+ **[Breaking]** Removes the failed test screenshot reporter in favor of the reporter included with `@wordpress/scripts`. -+ **[Breaking]** Failed test screenshots are now stored in the `tmp/artifacts` directory. -+ **[BREAKING]** Remove the default `DependencyExtractionWebpackPlugin` in favor of our custom loader from generated webpack configs. -+ Adds env var loading from `.llmsenv` with a fallback to `.llmsenv.dist`. The former file intended to be excluded from version control systems. -+ Adds a default `.eslintrc.js` configuration intended for use by LifterLMS and LifterLMS projects (via `wp-scripts lint-js`). - - -v2.0.0-beta.1 - 2021-09-10 --------------------------- - -+ **[Breaking]** Raised the minimum required `@wordpress/scripts` version to 17.1.0. -+ **[Breaking]** Removes the failed test screenshot reporter in favor of the reporter included with `@wordpress/scripts`. -+ **[Breaking]** Failed test screenshots are now stored in the `tmp/artifacts` directory. -+ Adds env var loading from `.llmsenv` with a fallback to `.llmsenv.dist`. The former file intended to be excluded from version control systems. -+ Adds a default `.eslintrc.js` configuration intended for use by LifterLMS and LifterLMS projects (via `wp-scripts lint-js`). - - -v1.3.3 - 2021-01-07 -------------------- - -+ Updated screenshot reporter function to include additional debugging information - - -v1.3.1 - 2020-08-11 -------------------- - -+ Don't use imports. - - -v1.3.0 - 2020-08-11 -------------------- - -+ Modify the `jest-puppeteer.config.js` to use defaults from `@wordpress/scripts`. - - -v1.2.4 - 2020-08-10 -------------------- - -+ Resolve script files for better portability. - - -v1.2.3 - 2020-08-10 -------------------- - -+ Add a configurable source file path option and set the default to `src/` instead of `assets/src` to the `webpack.config.js` generator. - - -v1.2.1 - 2020-07-21 -------------------- - -+ Update webpack config code for reduced complexity. - - -v1.2.0 - 2020-07-17 -------------------- - -+ Added webpack config "generator" method. diff --git a/packages/scripts/README.md b/packages/scripts/README.md deleted file mode 100644 index 77f7e383c6..0000000000 --- a/packages/scripts/README.md +++ /dev/null @@ -1,22 +0,0 @@ -LifterLMS Scripts -================= - -Test, build, and development scripts for LifterLMS projects. - -This 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. - -## Installation - -Install the module - -``` -npm install --save-dev @lifterlms/scripts -``` - -## CHANGELOG - -[CHANGELOG](./CHANGELOG.md) - -## API - -API Docs to be written. diff --git a/packages/scripts/config/.eslintrc.js b/packages/scripts/config/.eslintrc.js deleted file mode 100644 index e7810fdf57..0000000000 --- a/packages/scripts/config/.eslintrc.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Default eslint config for LifterLMS projects - * - * @package LifterLMS/Scripts/Config - * - * @since [version] - * @version [version] - */ - -const eslintConfig = { - root: true, - extends: [ - 'plugin:@wordpress/eslint-plugin/recommended-with-formatting', - ], - rules: { - 'jsdoc/tag-lines': [ 0 ], - 'jsdoc/require-jsdoc': 'error', - 'jsdoc/require-param-description': 'error', - 'jsdoc/require-returns': 'error', - }, - settings: { - // Ensure that WordPress core dependencies don't throw errors when importing them. - 'import/internal-regex': '^@wordpress/', - 'import/core-modules': [ - // @todo: This list needs to be expanded to include other WP Core included modules. - 'jquery', - ] - } -}; - -module.exports = eslintConfig; - - diff --git a/packages/scripts/config/jest-unit.config.js b/packages/scripts/config/jest-unit.config.js deleted file mode 100644 index 0ed28a7c65..0000000000 --- a/packages/scripts/config/jest-unit.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Main Jest config - * - * @since Unknown - * @version 2.0.0 - */ - - -const - // Import the initial config to be moified. - config = require( '@wordpress/scripts/config/jest-unit.config' ); - -// Set the root directory to the project's root. -config.rootDir = process.cwd(); - -/** - * Jest Config - * - * @link https://jestjs.io/docs/en/configuration.html - */ -module.exports = config; diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js deleted file mode 100644 index 0fac6a57db..0000000000 --- a/packages/scripts/config/webpack.config.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Webpack config - * - * @package LifterLMS_Groups/Scripts/Dev - * - * @since Unknown - * @version 2.1.0 - */ - -// Deps. -const - cssExtract = require( 'mini-css-extract-plugin' ), - cssRTL = require( 'webpack-rtl-plugin' ), - config = require( '@wordpress/scripts/config/webpack.config' ), - depExtract = require( '@wordpress/dependency-extraction-webpack-plugin' ), - path = require( 'path' ); - -/** - * Used by dependency extractor to handle requests to convert names of scripts included in the LifterLMS Core. - * - * @since 1.2.1 - * - * @param {string} request External script slug/id. - * @return {String|Array} A string - */ -function requestToExternal( request ) { - - if ( 'llms-quill' === request ) { - return 'Quill'; - } else if ( 'llms-izimodal' === request ) { - return [ 'jQuery', 'iziModal' ]; - } else if ( request.startsWith( 'llms/' ) || request.startsWith( 'LLMS/' ) ) { - return request.split( '/' ); - } - -} - -/** - * Used by dependency extractor to handle requests to scripts included in the LifterLMS Core. - * - * @since 1.2.1 - * - * @param {string} request External script slug/id. - * @return {String|Array} A string - */ -function requestToHandle( request ) { - if ( request.startsWith( 'llms/' ) || request.startsWith( 'LLMS/' ) ) { - return 'llms'; - } -} - -/** - * Configure the `entry` object of the webpack config file. - * - * @since 1.2.1 - * @since 1.2.3 Add a configurable source file path. - * - * @param {String[]} js Array of JS file slugs. - * @param {String} srcPath Relative path to the base source file directory. - * @return {Object} Webpack config entry object. - */ -function setupEntry( js, srcPath ) { - - const entry = {}; - js.forEach( file => { - entry[ file ] = path.resolve( process.cwd(), `${ srcPath }js/`, `${ file }.js` ); - } ); - - return entry; - -} - -/** - * Setup the `plugins` array of the webpack config file. - * - * @since 1.2.1 - * @since 2.0.0 Remove default DependencyExtractionWebpackPlugin in favor of our custom loader. - * @since 2.1.0 Added `cleanAfterEveryBuildPatterns` parameter. - * - * @param {Object[]} plugins Array of plugin objects or classes. - * @param {String[]} css Array of CSS file slugs. - * @param {String} prefix File prefix. - * @param {String[]} cleanAfterEveryBuildPatterns List of patterns added to the CleanWebpackPlugin config. - * @return {Object[]} Array of plugin objects or classes. - */ -function setupPlugins( plugins, css, prefix, cleanAfterEveryBuildPatterns ) { - - // Modify the CleanWebpackPlugin's cleanAfterEveryBuildPatterns config. - if ( cleanAfterEveryBuildPatterns.length ) { - - plugins = plugins.filter( plugin => { - - if ( 'CleanWebpackPlugin' === plugin.constructor.name ) { - - plugin.cleanAfterEveryBuildPatterns = [ - ...plugin.cleanAfterEveryBuildPatterns, - ...cleanAfterEveryBuildPatterns, - ]; - - } - - return plugin; - - } ); - - } - - const REMOVE_PLUGINS = [ - /** - * Remove the original WP Core dependency extractor. If we add an extractor - * without removing the initial one core dependencies get lost when our - * extractor runs. - */ - 'DependencyExtractionWebpackPlugin', - - /** - * Remove the css extractor implemented in the default config. - * - * Our CSS extractor puts things in our preferred directory structure. - */ - 'MiniCssExtractPlugin' - ]; - plugins = plugins.filter( plugin => ! REMOVE_PLUGINS.includes( plugin.constructor.name ) ); - - css.forEach( file => { - - // Extract CSS. - plugins.push( new cssExtract( { - filename: `css/${ prefix }[name].css`, - } ) ); - - // Generate an RTL CSS file. - plugins.push( new cssRTL( { - filename: `css/${ prefix }[name]-rtl.css`, - } ) ); - - } ); - - // Add a custom dependency extractor. - plugins.push( new depExtract( { - requestToExternal, - requestToHandle, - injectPolyfill: true, - } ) ); - - return plugins; - -} - -/** - * Generates a Webpack config object - * - * This is opinionated based on our opinions for directory structure. - * - * ESNext JS source files are located in `src/js`. - * - * SASS/SCSS source files are located in `src/sass`. - * - * SASS files should be imported via the JS source file. - * - * @since Unknown - * @since 1.2.1 Reduce method size by using helper methods - * @since 1.2.3 Add a configurable source file path option and set the default to `src/` instead of `assets/src`. - * @since 2.1.0 Add configuration option added to the CleanWebpackPlugin. - * - * @param {String[]} options.css Array of CSS file slugs. - * @param {String[]} options.js Array of JS file slugs. - * @param {String} options.prefix File prefix. - * @param {String} options.outputPath Relative path to the output directory. - * @param {String[]} options.cleanAfterEveryBuildPatterns List of patterns added to the CleanWebpackPlugin config. - * @return {Object} A webpack.config.js object. - */ -module.exports = ( - { - css = [], - js = [], - prefix = 'llms-', - outputPath = 'assets/', - srcPath = 'src/', - cleanAfterEveryBuildPatterns = [], - } -) => { - - return { - ...config, - entry: setupEntry( js, srcPath ), - output: { - filename: `js/${ prefix }[name].js`, - path: path.resolve( process.cwd(), outputPath ), - }, - plugins: setupPlugins( config.plugins, css, prefix, cleanAfterEveryBuildPatterns ), - }; - -} diff --git a/packages/scripts/e2e/bootstrap.js b/packages/scripts/e2e/bootstrap.js deleted file mode 100644 index be3c226204..0000000000 --- a/packages/scripts/e2e/bootstrap.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Tests Bootstrap. - * - * @since Unknown - * @version 2.0.0 - */ - -require( 'regenerator-runtime' ); - -const { existsSync } = require( 'fs' ), - { execSync } = require( 'child_process' ); - -// Load dotenv files. -const envFiles = [ '.llmsenv', '.llmsenv.dist' ]; -envFiles.some( ( file ) => { - const path = `${ process.cwd() }/${ file }`; - if ( existsSync( file ) ) { - require( 'dotenv' ).config( { path } ); - } -} ); - -if ( ! process.env.WP_VERSION ) { - - try { - const wpVersion = execSync( 'composer run env wp core version', { stdio : 'pipe' } ).toString(); - if ( wpVersion ) { - process.env.WP_VERSION = wpVersion; - } - } catch ( e ) { - console.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.' ); - process.env.WP_VERSION = 'latest'; - } - -} - -// Setup the WP Base URL for e2e Tests. -if ( ! process.env.WORDPRESS_PORT ) { - process.env.WORDPRESS_PORT = '8080'; -} - -// Allow easy override of the default base URL, for example if we want to point to a live URL. -if ( ! process.env.WP_BASE_URL ) { - process.env.WP_BASE_URL = `http://localhost:${ process.env.WORDPRESS_PORT }`; -} - -// Retry tests automatically to prevent against false positives. -jest.retryTimes( 2 ); - -// The Jest timeout is increased because these tests are a bit slow. -jest.setTimeout( process.env.PUPPETEER_TIMEOUT || 100000 ); - -beforeAll( async() => { - - page.on( 'console', ( message ) => { - if ( [ 'info', 'log' ].includes( message.type() ) ) { - return; - } - console.log( message.type(), message.text() ); - } ); - - page.on( 'pageerror', ( err ) => { - console.log( err.message ); - } ); - -} ); diff --git a/packages/scripts/e2e/global-teardown.js b/packages/scripts/e2e/global-teardown.js deleted file mode 100644 index a10087e27c..0000000000 --- a/packages/scripts/e2e/global-teardown.js +++ /dev/null @@ -1,44 +0,0 @@ -require( 'regenerator-runtime' ); - -const teardown = require( '@wordpress/scripts/config/jest-environment-puppeteer/teardown' ), - { existsSync, readdirSync, renameSync, rmSync, mkdirSync } = require( 'fs' ); - -/** - * Relocate artifacts from the root to the tmp directory. - * - * This can be removed if/when @wordpress/scripts allows configuration of the ARTIFACTS_PATH - * constant - * - * @since 2.0.0 - * - * @link https://github.com/WordPress/gutenberg/issues/34797 - * - * @return {void} - */ -module.exports = async () => { - - const defaultArtifacts = `${ process.cwd() }/artifacts`, - artifacts = `${ process.cwd() }/tmp/artifacts`; - - // We have artifacts to move. - if ( existsSync( defaultArtifacts ) ) { - - // Ensure our directory exists. - if ( ! existsSync( artifacts ) ) { - mkdirSync( artifacts, { recursive: true } ); - } - - // Move all the artifacts. - readdirSync( defaultArtifacts ).forEach( fileName => { - renameSync( `${ defaultArtifacts }/${ fileName }`, `${ artifacts }/${ fileName }` ) - } ); - - // Delete the original directory. - rmSync( defaultArtifacts + '/', { recursive: true, force: true } ); - - } - - // Run the original teardown from @wordpress/scripts. - await teardown(); - -}; diff --git a/packages/scripts/e2e/jest-puppeteer.config.js b/packages/scripts/e2e/jest-puppeteer.config.js deleted file mode 100644 index ac037db87e..0000000000 --- a/packages/scripts/e2e/jest-puppeteer.config.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Jest Puppeteer Config - * - * @since Unknown - * @version Unknown - * - * @link https://github.com/smooth-code/jest-puppeteer#jest-puppeteerconfigjs - */ - -const window = process.env.PUPPETEER_WINDOW || '1440x900', - dimensions = window.split( 'x' ).map( ( int ) => parseInt( int, 10 ) ); - -const config = { - launch: { - ignoreHTTPSErrors: true, - headless: process.env.PUPPETEER_HEADLESS !== 'false', - slowMo: parseInt( process.env.PUPPETEER_SLOWMO, 10 ) || 0, - defaultViewport: { - width: dimensions[ 0 ], - height: dimensions[ 1 ], - }, - }, - exitOnPageError: false, -}; - -if ( false === config.launch.headless ) { - config.launch.args = [ - `--window-size=${ dimensions[ 0 ] },${ dimensions[ 1 ] }`, - ]; -} - -module.exports = config; diff --git a/packages/scripts/e2e/jest.config.js b/packages/scripts/e2e/jest.config.js deleted file mode 100644 index b5ef0c47ac..0000000000 --- a/packages/scripts/e2e/jest.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Main Jest config - * - * @since Unknown - * @version 2.0.0 - */ - -/** - * Load the jest-puppeteer config file - * - * @see https://github.com/smooth-code/jest-puppeteer/issues/160#issuecomment-491975158 - */ -process.env.JEST_PUPPETEER_CONFIG = require.resolve( './jest-puppeteer.config.js' ); - -const - // Import the initial config to be moified. - config = require( '@wordpress/scripts/config/jest-e2e.config' ), - - // List of uncompiled es modlues. - esModules = [ '@lifterlms/llms-e2e-test-utils' ].join( '|' ); - -// Setup files. -config.setupFilesAfterEnv = [ - require.resolve( './bootstrap.js' ), -]; - -config.rootDir = process.cwd(); - -// Sort tests alphabetically by path. Ensures Tests in the "activate" directory run first. -config.testSequencer = require.resolve( './sequencer.js' ); - -// Look for tests with with ".test.js" as a suffix. -config.testMatch = [ '**/tests/**/*.test.[jt]s?(x)' ]; - -// Don't transform specified modules. -config.transformIgnorePatterns = [ `/node_modules/(?!${ esModules })` ]; - -/** - * Override the global teardown to remove assets from the root dir. - * - * This can be removed if the @wordpress/scripts ARTIFACT_PATH can be changed via env vars. - * - * @link https://github.com/WordPress/gutenberg/issues/34797 - */ -config.globalTeardown = require.resolve( './global-teardown' ); - -/** - * Jest Config - * - * @link https://jestjs.io/docs/en/configuration.html - */ -module.exports = config; diff --git a/packages/scripts/e2e/sequencer.js b/packages/scripts/e2e/sequencer.js deleted file mode 100644 index 75903cac31..0000000000 --- a/packages/scripts/e2e/sequencer.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Jest tests sequencer - * - * Runs our tests in alphabetical order by directory / filename. - * - * This allows us to do things like run the setup wizard tests to further bootstrap - * the testing environment for other tests. - * - * @since Unknown - * @version Unknown - * - * @link https://jestjs.io/docs/en/next/configuration#testsequencer-string - */ - -const Sequencer = require('@jest/test-sequencer').default; - -class CustomSequencer extends Sequencer { - sort( tests ) { - const copyTests = Array.from( tests ); - return copyTests.sort( ( testA, testB ) => ( testA.path > testB.path ? 1 : -1 ) ); - } -} - -module.exports = CustomSequencer; diff --git a/packages/scripts/package.json b/packages/scripts/package.json deleted file mode 100644 index 566e1f2fe5..0000000000 --- a/packages/scripts/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@lifterlms/scripts", - "version": "2.2.0", - "description": "Test, build, and development scripts for LifterLMS projects.", - "author": "Team LifterLMS <dev@lifterlms.com>", - "license": "GPL-3.0-or-later", - "homepage": "https://github.com/gocodebox/lifterlms/tree/master/packages/scripts", - "keywords": [ - "lifterlms", - "wordpress", - "scripts", - "utils" - ], - "repository": { - "type": "git", - "url": "https://github.com/gocodebox/lifterlms.git", - "directory": "packages/scripts" - }, - "bugs": { - "url": "https://github.com/gocodebox/lifterlms/labels/package%3A%20scripts" - }, - "main": "src/index.js", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@jest/test-sequencer": "^27.4.6", - "@wordpress/scripts": "^20.0.2", - "dotenv": "^8.2.0", - "webpack-rtl-plugin": "^2.0.0" - }, - "scripts": { - "dev": "./../dev/src/index.js" - } -} diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 2457ae8fbe..0000000000 --- a/phpcs.xml +++ /dev/null @@ -1,127 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="LifterLMS Core"> - <description>LifterLMS Rules for PHP_CodeSniffer</description> - - <file>.</file> - - <!-- Exclude project directories --> - <exclude-pattern>.bin/</exclude-pattern> - <exclude-pattern>.config/</exclude-pattern> - <exclude-pattern>.github/</exclude-pattern> - <exclude-pattern>.wordpress-org/</exclude-pattern> - - - <!-- Exclude Compiled JS files --> - <exclude-pattern>assets/js/llms.js</exclude-pattern> - <exclude-pattern>assets/js/llms-builder.js</exclude-pattern> - <exclude-pattern>assets/js/llms-metaboxes.js</exclude-pattern> - - <!-- Exclude node packages --> - <exclude-pattern>packages/</exclude-pattern> - - <!-- Exclude external libraries --> - <exclude-pattern>libraries/</exclude-pattern> - - <!-- Exclude deprecated/legacy files --> - <exclude-pattern>includes/functions/llms-functions-deprecated.php</exclude-pattern> - - <!-- Don't throw errors for this 3rd party library --> - <rule ref="WordPress.DB.PreparedSQL.InterpolatedNotPrepared"> - <exclude-pattern>includes/libraries/wp-background-processing/wp-background-process.php</exclude-pattern> - </rule> - - <!-- Exclude locale files that take forever to process --> - <exclude-pattern>languages/*.php</exclude-pattern> - - <rule ref="LifterLMS"> - - <!-- @todo: Apply coding standards to js --> - <exclude-pattern>assets/js/*.js</exclude-pattern> - - <!-- @todo: Fix docs and comments to adhere to these rules --> - - <exclude name="Squiz.Commenting.FunctionComment.ParamCommentFullStop" /> - - <exclude name="Generic.Commenting.DocComment.MissingShort" /> - <exclude name="Generic.Commenting.DocComment.ShortNotCapital" /> - - <exclude name="Squiz.Commenting.FunctionComment.Missing" /> - <exclude name="Squiz.Commenting.FunctionComment.MissingParamComment" /> - <exclude name="Squiz.Commenting.FunctionComment.MissingParamTag" /> - <exclude name="Squiz.Commenting.FunctionComment.MissingParamName" /> - <exclude name="Squiz.Commenting.VariableComment.Missing" /> - - <exclude name="Squiz.Commenting.FunctionComment.InvalidReturnVoid" /> - - <!-- @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 --> - <exclude name="WordPress.WP.GlobalVariablesOverride.Prohibited" /> - - <!-- @todo: extract is messy you're right, fix this --> - <exclude name="WordPress.PHP.DontExtract.extract_extract" /> - - <!-- @todo: Most core files break this rule. --> - <exclude name="WordPress.Files.FileName.InvalidClassFileName" /> - - <!-- @todo: This needs to be adjusted since WP 5.3 --> - <exclude name="WordPress.DateTime.RestrictedFunctions.date_date" /> - - <!-- These templates follow WP Template style so they're okay --> - <exclude name="WordPress.Files.FileName.NotHyphenatedLowercase"> - <exclude-pattern>templates/taxonomy-*.php</exclude-pattern> - </exclude> - - </rule> - - <!-- - @todo The following 3 rule sets are disabled for the following files/directories - We are in the process of gradually fixing these in bulk. - See https://github.com/gocodebox/lifterlms/issues/946 - --> - <rule ref="LifterLMS.Commenting.FileComment"> - <exclude-pattern>includes/admin/views/*.php</exclude-pattern> - <exclude-pattern>includes/admin/views/**/*.php</exclude-pattern> - - <exclude-pattern>templates/*.php</exclude-pattern> - <exclude-pattern>templates/**/*.php</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.FileComment"> - <exclude-pattern>includes/admin/views/*.php</exclude-pattern> - <exclude-pattern>includes/admin/views/**/*.php</exclude-pattern> - - <exclude-pattern>templates/*.php</exclude-pattern> - <exclude-pattern>templates/**/*.php</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.ClassComment.Missing"> - <exclude-pattern>includes/admin/views/*.php</exclude-pattern> - <exclude-pattern>includes/admin/views/**/*.php</exclude-pattern> - - <exclude-pattern>templates/*.php</exclude-pattern> - <exclude-pattern>templates/**/*.php</exclude-pattern> - </rule> - - <rule ref="Squiz.Commenting.InlineComment.InvalidEndChar"> - <!-- To be fixed --> - <exclude-pattern>includes/notifications/class.llms.notifications.query.php</exclude-pattern> - <exclude-pattern>includes/privacy/class-llms-privacy-exporters.php</exclude-pattern> - <exclude-pattern>includes/privacy/class-llms-privacy.php</exclude-pattern> - <exclude-pattern>includes/processors/class.llms.processor.membership.bulk.enroll.php</exclude-pattern> - <exclude-pattern>includes/processors/class.llms.processor.table.to.csv.php</exclude-pattern> - <exclude-pattern>includes/shortcodes/class.llms.shortcode.course.outline.php</exclude-pattern> - <exclude-pattern>includes/shortcodes/class.llms.shortcode.hide.content.php</exclude-pattern> - </rule> - - <rule ref="WordPress.WP.I18n"> - <!-- @todo: Fix all of these --> - <exclude name="WordPress.WP.I18n.MissingTranslatorsComment" /> - - <properties> - <property name="text_domain" value="lifterlms" /> - </properties> - </rule> - - <!-- @todo: Fix these issues. --> - <rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops.Found"> - <exclude-pattern>assets/js/*.js</exclude-pattern> - </rule> - -</ruleset> diff --git a/phpmd.xml b/phpmd.xml deleted file mode 100644 index 244d74744d..0000000000 --- a/phpmd.xml +++ /dev/null @@ -1,48 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="WordPress LifterLMS" - xmlns="http://pmd.sf.net/ruleset/1.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" - xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> - <description>LifterLMS PHPMD Standards</description> - - <rule ref="rulesets/cleancode.xml"> - - <!-- used all over --> - <exclude name="BooleanArgumentFlag" /> - - <!-- in lack of real namespacing --> - <exclude name="StaticAccess" /> - - <!-- I disagree with this --> - <exclude name="ElseExpression" /> - - </rule> - - - <rule ref="rulesets/codesize.xml" /> - - <rule ref="rulesets/design.xml"> - - <!-- normal in WP for redirects, etc --> - <exclude name="ExitExpression" /> - - </rule> - - - <rule ref="rulesets/naming.xml/ShortVariable"> - <properties> - <property name="exceptions" value="id,wp,i" /> - </properties> - </rule> - - - <rule ref="rulesets/naming.xml/LongVariable" /> - <rule ref="rulesets/naming.xml/ShortMethodName" /> - <rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass" /> - <rule ref="rulesets/naming.xml/ConstantNamingConventions" /> - <rule ref="rulesets/naming.xml/BooleanGetMethodName" /> - - <rule ref="rulesets/unusedcode.xml" /> - -</ruleset> diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 18e0ba76ca..0000000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<phpunit - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/|version|/phpunit.xsd" - backupGlobals="false" - bootstrap="tests/phpunit/bootstrap.php" - cacheResultFile="tmp/.phpunit.result.cache" - colors="true" - convertErrorsToExceptions="true" - convertNoticesToExceptions="true" - convertWarningsToExceptions="true" - timeoutForSmallTests="1" - timeoutForMediumTests="10" - timeoutForLargeTests="60" - verbose="true"> - - <testsuites> - <testsuite name="LifterLMS Test Suite"> - <directory suffix=".php">tests/phpunit/unit-tests</directory> - </testsuite> - </testsuites> - - <filter> - <whitelist addUncoveredFilesFromWhitelist="true"> - <directory suffix=".php">.</directory> - <exclude> - <directory suffix="index.php">.</directory> - <directory suffix=".php">./admin/views/</directory> - <directory suffix=".php">./dist/</directory> - <directory suffix=".php">./node_modules/</directory> - <directory suffix=".php">./templates/</directory> - <directory suffix=".php">./tests/</directory> - <directory suffix=".php">./tmp/</directory> - <directory suffix=".php">./vendor/</directory> - <directory suffix=".php">./wordpress/</directory> - </exclude> - </whitelist> - </filter> - -</phpunit> diff --git a/readme.txt b/readme.txt index ef2aed730d..f2b87b560e 100644 --- a/readme.txt +++ b/readme.txt @@ -7,7 +7,7 @@ License URI: https://www.gnu.org/licenses/gpl-3.0.html Requires at least: 5.5 Tested up to: 5.9 Requires PHP: 7.3 -Stable tag: 5.9.0 +Stable tag: 5.10.0 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. @@ -538,6 +538,47 @@ You can review our full security policy at [https://lifterlms.com/security-polic == Changelog == += v5.10.0 - 2022-02-22 = + +##### Updates and Enhancements + ++ 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) ++ Added an option to specify a custom checkout form title for free access plans. [#1774](https://github.com/gocodebox/lifterlms/issues/1774) + +##### Bug Fixes + ++ Fixed ability to sort course students table by completed date. [#1969](https://github.com/gocodebox/lifterlms/issues/1969) ++ Fixed issue when the course has no lessons. [#2012](https://github.com/gocodebox/lifterlms/issues/2012) ++ Fixed broken checkout on Twenty Twenty-Two Theme when using password strength meter. [#1997](https://github.com/gocodebox/lifterlms/issues/1997) ++ Fixed block template slug generation from path on Windows. [#2001](https://github.com/gocodebox/lifterlms/issues/2001) ++ Fixed an issue encountered when using the search box on the voucher admin posts list screen. [#2005](https://github.com/gocodebox/lifterlms/issues/2005) + +##### Updated Templates + ++ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/courses/overview.php) ++ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/memberships/overview.php) ++ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/quizzes/overview.php) ++ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/archive-course.html) ++ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/archive-llms_membership.html) ++ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/single-certificate.html) ++ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/single-no-access.html) ++ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_cat.html) ++ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_difficulty.html) ++ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_tag.html) ++ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_track.html) ++ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-membership_cat.html) ++ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-membership_tag.html) ++ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/checkout/form-confirm-payment.php) ++ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/lesson-navigation.php) ++ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/lesson-preview.php) ++ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/parent-course.php) ++ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/loop-main.php) ++ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/loop.php) ++ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/myaccount/view-order.php) ++ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/quiz/questions/content-picture_choice.php) ++ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/quiz/results.php) + + = v5.9.0 - 2022-02-15 = ##### Updates and Enhancements @@ -747,17 +788,4 @@ You can review our full security policy at [https://lifterlms.com/security-polic + Fixed issue causing the latest achievement to not display when reviewing grades on the student dashboard. -= v5.3.1 - 2021-09-13 = - -##### Bug fixes - -+ Fixed quote slashing for non-admin roles when editing content in the course builder. -+ The LifterLMS admin icon now uses an encoded SVG to improve admin color scheme compatibility. -+ Fixed an issue with empty admin notices. - -##### Dev updates - -+ The creation date of `llms_orders` is now determined by `llms_current_time()`. - - [Read the full changelog](https://make.lifterlms.com/tag/lifterlms) diff --git a/src/js/.eslintrc.js b/src/js/.eslintrc.js deleted file mode 100644 index 523f228d3b..0000000000 --- a/src/js/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - rules: { - // This conflicts with PHPCS sniff for the fileheader comment: @parckage in JSDoc should be empty. - 'jsdoc/empty-tags': 'off', - }, -}; diff --git a/src/js/admin-addons.js b/src/js/admin-addons.js deleted file mode 100644 index e6023919ff..0000000000 --- a/src/js/admin-addons.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * UI & UX for the Admin add-ons management screen - * - * @package LifterLMS/Scripts/Admin - * - * @since 3.22.0 - * @version 5.5.0 - */ - -import { _n, sprintf } from '@wordpress/i18n'; -import $ from 'jquery'; -import '../scss/admin-addons.scss'; - -( function() { - /** - * Tracks current # of each bulk action to be run upon form submission - * - * @type {Object} - */ - const actions = { - update: 0, - install: 0, - activate: 0, - deactivate: 0, - }; - - /** - * When the bulk action modal is closed, clear all existing staged actions - * - * @since 3.22.0 - */ - $( '.llms-bulk-close' ).on( 'click', function( e ) { - e.preventDefault(); - $( 'input.llms-bulk-check' ).filter( ':checked' ).prop( 'checked', false ).trigger( 'change' ); - } ); - - /** - * Update the UI and counters when a checkbox action is changed - * - * @since 3.22.0 - */ - $( 'input.llms-bulk-check' ).on( 'change', function() { - const action = $( this ).attr( 'data-action' ); - - if ( $( this ).is( ':checked' ) ) { - actions[ action ]++; - } else { - actions[ action ]--; - } - - updateUserInterface(); - } ); - - /** - * Updates the UI when bulk actions are changed. - * - * Shows # of each action to be applied & shows the form submission / cancel buttons - * - * @since 3.22.0 - * @since 5.5.0 Use `wp.i18n` functions in favor of `LLMS.l10n` and use `$.text()` in favor of `$.html()`. - * Renamed from `update_ui()` to match coding standards. - * - * @return {void} - */ - function updateUserInterface() { - const $el = $( '#llms-addons-bulk-actions' ); - if ( actions.update || actions.install || actions.activate || actions.deactivate ) { - $el.addClass( 'active' ); - } else { - $el.removeClass( 'active' ); - } - - $.each( actions, function( key, count ) { - const $desc = $el.find( '.llms-bulk-desc.' + key ); - - let text = ''; - - if ( count ) { - // Translators: %d = Number of add-ons to perform the specified action against. - text = sprintf( _n( '%d add-on', '%d add-ons', count, 'lifterlms' ), count ); - $desc.show(); - } else { - $desc.hide(); - } - $desc.find( 'span' ).text( text ); - } ); - } - - /** - * Show the keys management dropdown on click of the "My License Keys" button - * - * @since 3.22.0 - */ - $( '#llms-active-keys-toggle' ).on( 'click', function() { - $( '#llms-key-field-form' ).toggle(); - } ); -}() ); diff --git a/src/scss/admin-addons.scss b/src/scss/admin-addons.scss deleted file mode 100644 index 56fef81ad1..0000000000 --- a/src/scss/admin-addons.scss +++ /dev/null @@ -1,268 +0,0 @@ -@import '@lifterlms/brand/sass/colors'; -@import "../../assets/scss/_includes/mixins"; - -.wrap.lifterlms-addons { - - .wp-heading-inline { - vertical-align: middle; - } - - .llms-nav-tab-wrapper.llms-nav-text .llms-nav-item:after { - margin: 0 3px; - } - -} - -.llms-addons-bulk-actions { - background: #fff; - border: 1px solid #ddd; - box-shadow: inset 0 1px 0 rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.1); - padding: 40px; - left: 50%; - margin-left: -100px; - position: fixed; - text-align: center; - transition: top 0.2s ease; - top: -100%; - width: 240px; - z-index: 1; - &.active { - top: 80px; - } - - .llms-bulk-close { - background: #fff; - border: 1px solid #ddd; - border-bottom-width: 0; - border-left-width: 0; - border-radius: 50%; - color: llms-color( wp-red-50 ); - font-size: 25px; - height: 25px; - padding: 5px; - position: absolute; - right: -10px; - top: -10px; - width: 25px; - } - - .llms-bulk-desc { - font-size: 18px; - margin-bottom: 20px; - .fa { - color: llms-color( llms-blue ); - display: block; - font-size: 30px; - margin-bottom: 10px; - } - &.deactivate .fa { - color: #777; - } - } - -} - -.llms-addons-wrap { - - @include clearfix(); - - .llms-add-on-item { - display: block; - list-style: none; - margin-bottom: 20px; - position: relative; - - @media only screen and ( min-width: 680px ) { - float: left; - margin-right: 30px; - width: calc( 33.333% - 20px ); - &:nth-of-type(3n) { - margin-right: 0; - } - &:nth-of-type(3n+1) { - clear: left; - } - } - - } - - .llms-add-on { - - background-color: #f5f5f5; - border: 1px solid #ddd; - box-shadow: inset 0 1px 0 rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.1); - position: relative; - - .llms-add-on-link { - color: #444; - display: block; - text-decoration: none; - } - - header { - margin-bottom: 0; - h4 { - color: #23282d; - font-size: 16px; - margin: 8px 8px 0; - } - img { - display: block; - max-width: 100%; - } - } - - section { - padding: 8px; - p { - margin: 0 0 8px; - } - ul, li { - margin: 0; - padding: 0; - } - img { - border-radius: 50%; - display: inline-block; - width: 18px; - vertical-align: bottom; - } - } - - footer.llms-actions { - background: #e8e8e8; - border-top: 1px solid #ddd; - padding: 16px 8px; - margin-top: 8px; - - a.open-plugin-details-modal { - font-size: 18px; - padding: 5px; - vertical-align: middle; - } - - .llms-status-icon { - - background: #ececec; - border: 1px solid #b7b7b7; - color: #444; - border-radius: 4px; - margin-right: 4px; - padding: 8px; - text-decoration: none; - vertical-align: middle; - - &:hover { - background: #f0f0f0; - .fa.show-on-hover { display: inline-block; } - .fa.hide-on-hover { display: none; } - } - - .fa { - color: #777; - display: inline-block; - font-size: 16px; - height: 16px; - text-align: center; - margin-right: 2px; - width: 16px; - } - - .fa.show-on-hover { display: none; } - .fa.hide-on-hover { display: inline-block; } - - input, - input + .fa { - display: none; - color: llms-color( llms-blue ) !important; - } - - input:checked + .fa { - display: inline-block; - & + .fa { - display: none; - } - } - - .llms-status-text { - font-weight: 400; - font-size: 14px; - } - - - &.status--installed, - &.status--license_active { - .fa { - color: llms-color( wp-green-50 ); - } - } - - // &.status--uninstalled, - &.status--active, - &.status--license_inactive { - .fa { - color: llms-color( wp-red-50 ); - } - } - - &.external.status--none, - &.external.status--license_active, // fixes xapi - &.external.status--license_inactive { // fixes xapi - .fa { - color: llms-color( llms-blue ); - } - } - - } - - .llms-button-secondary { - border: 1px solid #b7b7b7; - border-radius: 4px; - float: right; - &:hover { - background: #f0f0f0; - } - } - - .llms-addon-actions { - background: #f0f0f0; - border: 1px solid #b7b7b7; - box-shadow: inset 0 1px 0 rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.1); - display: none; - left: 16px; - margin: 0; - padding: 16px; - position: absolute; - right: 16px; - z-index: 1; - &:before, &:after { - content: ''; - position: absolute; - } - &:before { - border: 10px solid transparent; - border-bottom-color: #b7b7b7; - position: absolute; - top: -20px; - right: 34px; - } - &:after { - border: 8px solid transparent; - border-bottom-color: #f0f0f0; - top: -16px; - right: 36px; - } - - li { - margin-bottom: 8px; - &:last-child { - margin-bottom: 0; - } - } - } - - } - - } - -} diff --git a/tests/assets/christian-fregnan-unsplash.jpg b/tests/assets/christian-fregnan-unsplash.jpg deleted file mode 100644 index 20261b2010..0000000000 Binary files a/tests/assets/christian-fregnan-unsplash.jpg and /dev/null differ diff --git a/tests/assets/example-style-1.css b/tests/assets/example-style-1.css deleted file mode 100644 index 23fd2f2d35..0000000000 --- a/tests/assets/example-style-1.css +++ /dev/null @@ -1,4 +0,0 @@ -.llms-sample-selector-for-tests-only-1 { - color: white; -} - diff --git a/tests/assets/example-style-2.css b/tests/assets/example-style-2.css deleted file mode 100644 index 7481aa2edf..0000000000 --- a/tests/assets/example-style-2.css +++ /dev/null @@ -1,3 +0,0 @@ -.llms-sample-selector-for-tests-only-2 { - color: black; -} diff --git a/tests/assets/example-style.css b/tests/assets/example-style.css deleted file mode 100644 index 5cc24be354..0000000000 --- a/tests/assets/example-style.css +++ /dev/null @@ -1,4 +0,0 @@ -.llms-sample-selector-for-tests-only { - color: red; -} - diff --git a/tests/assets/import-error.json b/tests/assets/import-error.json deleted file mode 100644 index 9c806dc5d4..0000000000 --- a/tests/assets/import-error.json +++ /dev/null @@ -1 +0,0 @@ -{"_generator": "LifterLMS/SingleCourseExporter"} \ No newline at end of file diff --git a/tests/assets/import-fake-generator.json b/tests/assets/import-fake-generator.json deleted file mode 100644 index 2869d19913..0000000000 --- a/tests/assets/import-fake-generator.json +++ /dev/null @@ -1 +0,0 @@ -{"_generator": "LifterLMS/FakeGenerator"} diff --git a/tests/assets/import-with-prerequisites.json b/tests/assets/import-with-prerequisites.json deleted file mode 100644 index fb174c365b..0000000000 --- a/tests/assets/import-with-prerequisites.json +++ /dev/null @@ -1,224 +0,0 @@ -{ - "_generator": "LifterLMS/BulkCourseExporter", - "_source": "https://llms.test", - "_version": "4.5.1", - "courses": [ - { - "access_plans": [], - "audio_embed": "", - "author": 1, - "average_grade": 0, - "average_progress": 0, - "capacity": 0, - "capacity_message": "", - "categories": [], - "comment_status": "open", - "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", - "content_restricted_message": "", - "course_closed_message": "", - "course_opens_message": "", - "custom": { - "_llms_blocks_migrated": [ - "yes" - ] - }, - "date": "2020-10-14 22:40:28", - "date_gmt": "2020-10-14 22:40:28", - "difficulty": "", - "enable_capacity": "no", - "end_date": "", - "enrollment_closed_message": "", - "enrollment_end_date": "", - "enrollment_opens_message": "", - "enrollment_period": "no", - "enrollment_start_date": "", - "excerpt": "", - "featured_image": "", - "has_prerequisite": "yes", - "id": 1104, - "length": "", - "menu_order": 0, - "modified": "2020-10-14 22:40:28", - "modified_gmt": "2020-10-14 22:40:28", - "name": "course-with-prerequisites", - "password": "", - "permalink": "https://llms.test/course/course-with-prerequisites/", - "ping_status": "closed", - "prerequisite": 1102, - "prerequisite_track": 0, - "sales_page_content_page_id": 0, - "sales_page_content_type": "", - "sales_page_content_url": "", - "sections": [ - { - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-10-14 22:40:52", - "date_gmt": "2020-10-14 22:40:52", - "excerpt": "", - "id": 1106, - "lessons": [ - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-10-14 22:40:52", - "date_available": "", - "date_gmt": "2020-10-14 22:40:52", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "no", - "id": 1107, - "menu_order": 0, - "modified": "2020-10-14 22:40:53", - "modified_gmt": "2020-10-14 22:40:53", - "name": "new-lesson-9", - "order": 1, - "parent_course": 1104, - "parent_section": 1106, - "password": "", - "permalink": "https://llms.test/lesson/new-lesson-9/", - "ping_status": "closed", - "points": 1, - "prerequisite": 0, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "New Lesson", - "type": "lesson", - "video_embed": "" - }, - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-10-14 22:41:09", - "date_available": "", - "date_gmt": "2020-10-14 22:41:09", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "yes", - "id": 1111, - "menu_order": 0, - "modified": "2020-10-14 22:41:10", - "modified_gmt": "2020-10-14 22:41:10", - "name": "new-lesson-11", - "order": 2, - "parent_course": 1104, - "parent_section": 1106, - "password": "", - "permalink": "https://llms.test/lesson/new-lesson-11/", - "ping_status": "closed", - "points": 1, - "prerequisite": 1107, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "New Lesson", - "type": "lesson", - "video_embed": "" - } - ], - "menu_order": 0, - "modified": "2020-10-14 22:40:52", - "modified_gmt": "2020-10-14 22:40:52", - "name": "1106", - "order": 1, - "parent_course": 1104, - "password": "", - "ping_status": "closed", - "status": "publish", - "title": "New Section", - "type": "section" - } - ], - "start_date": "", - "status": "publish", - "tags": [], - "temp_calc_data": [], - "tile_featured_video": "no", - "time_period": "no", - "title": "Course with Prerequisites", - "tracks": [], - "type": "course", - "video_embed": "" - }, - { - "access_plans": [], - "audio_embed": "", - "author": 1, - "average_grade": 0, - "average_progress": 0, - "capacity": 0, - "capacity_message": "", - "categories": [], - "comment_status": "open", - "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", - "content_restricted_message": "", - "course_closed_message": "", - "course_opens_message": "", - "custom": { - "_llms_blocks_migrated": [ - "yes" - ] - }, - "date": "2020-10-14 22:39:39", - "date_gmt": "2020-10-14 22:39:39", - "difficulty": "", - "enable_capacity": "no", - "end_date": "", - "enrollment_closed_message": "", - "enrollment_end_date": "", - "enrollment_opens_message": "", - "enrollment_period": "no", - "enrollment_start_date": "", - "excerpt": "", - "featured_image": "", - "has_prerequisite": "no", - "id": 1102, - "length": "", - "menu_order": 0, - "modified": "2020-10-14 22:39:39", - "modified_gmt": "2020-10-14 22:39:39", - "name": "a-prerequisite-course", - "password": "", - "permalink": "https://llms.test/course/a-prerequisite-course/", - "ping_status": "closed", - "prerequisite": 0, - "prerequisite_track": 0, - "sales_page_content_page_id": 0, - "sales_page_content_type": "", - "sales_page_content_url": "", - "sections": [], - "start_date": "", - "status": "publish", - "tags": [], - "temp_calc_data": [], - "tile_featured_video": "no", - "time_period": "no", - "title": "A Prerequisite Course", - "tracks": [], - "type": "course", - "video_embed": "" - } - ] -} diff --git a/tests/assets/import-with-quiz.json b/tests/assets/import-with-quiz.json deleted file mode 100644 index 555a2ca95e..0000000000 --- a/tests/assets/import-with-quiz.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "_generator": "LifterLMS/SingleCourseExporter", - "_source": "http://localhost:8080", - "_version": "4.3.0", - "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": "", - "author": 1, - "average_grade": 0, - "average_progress": 0, - "capacity": 25, - "capacity_message": "Enrollment has closed because the maximum number of allowed students has been reached.", - "categories": [], - "comment_status": "open", - "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 /-->", - "content_restricted_message": "You must enroll in this course to access course content.", - "course_closed_message": "This course closed on [lifterlms_course_info id=\"98\" key=\"end_date\"].", - "course_opens_message": "This course opens on [lifterlms_course_info id=\"98\" key=\"start_date\"].", - "custom": { - "_llms_blocks_migrated": [ - "yes" - ], - "_custom_key": [ - "custom value", - "second val" - ] - }, - "date": "2020-08-04 23:09:49", - "date_gmt": "2020-08-04 23:09:49", - "difficulty": "Hardmode", - "enable_capacity": "yes", - "end_date": "", - "enrollment_closed_message": "Enrollment in this course closed on [lifterlms_course_info id=\"98\" key=\"enrollment_end_date\"].", - "enrollment_end_date": "", - "enrollment_opens_message": "Enrollment in this course opens on [lifterlms_course_info id=\"98\" key=\"enrollment_start_date\"].", - "enrollment_period": "no", - "enrollment_start_date": "", - "excerpt": "", - "featured_image": "", - "has_prerequisite": "no", - "id": 98, - "instructors": [ - { - "label": "Author", - "visibility": "visible", - "id": 1, - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "last_name": "" - } - ], - "length": "", - "menu_order": 0, - "modified": "2020-08-04 23:09:49", - "modified_gmt": "2020-08-04 23:09:49", - "name": "simple-course-with-quiz", - "password": "", - "permalink": "http://localhost:8080/course/simple-course-with-quiz/", - "ping_status": "closed", - "prerequisite": 0, - "prerequisite_track": 0, - "sales_page_content_page_id": 0, - "sales_page_content_type": "none", - "sales_page_content_url": "", - "sections": [ - { - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 1, - "last_name": "" - }, - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-08-04 23:10:08", - "date_gmt": "2020-08-04 23:10:08", - "excerpt": "", - "id": 100, - "lessons": [ - { - "audio_embed": "", - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 1, - "last_name": "" - }, - "comment_status": "closed", - "content": "This is the content of the lesson.", - "custom": { - "_custom_key": [ - "custom value", - "second val" - ] - }, - "date": "2020-08-04 23:10:08", - "date_available": "", - "date_gmt": "2020-08-04 23:10:08", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "yes", - "has_prerequisite": "no", - "id": 101, - "menu_order": 0, - "modified": "2020-08-04 23:10:08", - "modified_gmt": "2020-08-04 23:10:08", - "name": "new-lesson", - "order": 1, - "parent_course": 98, - "parent_section": 100, - "password": "", - "permalink": "http://localhost:8080/lesson/new-lesson/", - "ping_status": "closed", - "points": 25, - "prerequisite": 0, - "quiz": { - "allowed_attempts": 202, - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 1, - "last_name": "" - }, - "comment_status": "closed", - "content": "Take the quiz!", - "custom": { - "_custom_key": [ - "custom value", - "second val" - ] - }, - "date": "2020-08-04 23:10:23", - "date_gmt": "2020-08-04 23:10:23", - "excerpt": "", - "id": 103, - "lesson_id": 101, - "limit_attempts": "yes", - "limit_time": "no", - "menu_order": 0, - "modified": "2020-08-04 23:10:23", - "modified_gmt": "2020-08-04 23:10:23", - "name": "new-lesson-quiz", - "passing_percent": 85, - "password": "", - "permalink": "http://localhost:8080/quiz/new-lesson-quiz/", - "ping_status": "closed", - "questions": [ - { - "choices": [ - { - "id": "5f29eadfb0312", - "choice": "Blue", - "choice_type": "text", - "correct": false, - "marker": "A", - "question_id": "104", - "type": "choice" - }, - { - "id": "5f29eaeb91ebe", - "choice": "Red", - "choice_type": "text", - "correct": false, - "marker": "B", - "question_id": "104", - "type": "choice" - }, - { - "id": "5f29eaf54a910", - "choice": "Green", - "choice_type": "text", - "correct": true, - "marker": "C", - "question_id": "104", - "type": "choice" - } - ], - "clarifications": "Enhance!", - "clarifications_enabled": "yes", - "comment_status": "closed", - "content": "<p>There are more colors than this, of course.</p>\n", - "date_gmt": "2020-08-04 23:10:23", - "description_enabled": "yes", - "id": 104, - "image": [], - "menu_order": 1, - "modified_gmt": "2020-08-04 23:10:23", - "multi_choices": "no", - "name": "what-is-your-favorite-color", - "parent_id": 103, - "password": "", - "ping_status": "closed", - "points": 125, - "question": "", - "question_type": "choice", - "title": "What is your Favorite Color?", - "type": "llms_question", - "video_enabled": "no", - "video_src": "" - } - ], - "random_questions": "yes", - "show_correct_answer": "no", - "status": "publish", - "time_limit": 300, - "title": "New Lesson Quiz", - "type": "llms_quiz" - }, - "quiz_enabled": "yes", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "New Lesson", - "type": "lesson", - "video_embed": "" - } - ], - "menu_order": 0, - "modified": "2020-08-04 23:10:08", - "modified_gmt": "2020-08-04 23:10:08", - "name": "100", - "order": 1, - "parent_course": 98, - "password": "", - "ping_status": "closed", - "status": "publish", - "title": "New Section", - "type": "section" - } - ], - "start_date": "", - "status": "publish", - "tags": [], - "temp_calc_data": [], - "tile_featured_video": "no", - "time_period": "no", - "title": "Simple Course with Quiz", - "tracks": [], - "type": "course", - "video_embed": "" -} diff --git a/tests/assets/import-with-restrictions.json b/tests/assets/import-with-restrictions.json deleted file mode 100644 index d5e439466a..0000000000 --- a/tests/assets/import-with-restrictions.json +++ /dev/null @@ -1,483 +0,0 @@ -{ - "_generator": "LifterLMS/SingleCourseExporter", - "_source": "https://llms.test", - "_version": "4.3.0", - "access_plans": [], - "audio_embed": "", - "author": 1, - "av_prog_auto_advance": "global", - "av_prog_auto_play": "global", - "av_prog_require_completion": "global", - "av_vimeo_player_disable_controls": "global", - "av_vimeo_player_disable_speed": "global", - "average_grade": 0, - "average_progress": 0, - "capacity": 0, - "capacity_message": "Enrollment has closed because the maximum number of allowed students has been reached.", - "categories": [], - "comment_status": "open", - "content": "<!-- wp:html -->\n<strong id=\"enrolled-user-content\">Enrolled user content.</strong>\n<!-- /wp:html -->\n<!-- wp:llms/course-syllabus /-->", - "content_restricted_message": "You must enroll in this course to access course content.", - "course_closed_message": "This course closed on [lifterlms_course_info id=\"85\" key=\"end_date\"].", - "course_opens_message": "This course opens on [lifterlms_course_info id=\"85\" key=\"start_date\"].", - "custom": { - "_llms_generated_from_id": [ - "85" - ], - "_llms_blocks_migrated": [ - "yes" - ], - "_llms_reviews_enabled": [ - "" - ], - "_llms_display_reviews": [ - "" - ], - "_llms_num_reviews": [ - "" - ], - "_llms_multiple_reviews_disabled": [ - "" - ] - }, - "date": "2020-08-05 18:34:15", - "date_gmt": "2020-08-05 18:34:15", - "difficulty": "", - "enable_capacity": "no", - "end_date": "", - "enrollment_closed_message": "Enrollment in this course closed on [lifterlms_course_info id=\"85\" key=\"enrollment_end_date\"].", - "enrollment_end_date": "", - "enrollment_opens_message": "Enrollment in this course opens on [lifterlms_course_info id=\"85\" key=\"enrollment_start_date\"].", - "enrollment_period": "no", - "enrollment_start_date": "", - "excerpt": "<strong id=\"non-enrolled-user-content\">Non-enrolled user content.</strong>", - "featured_image": "", - "has_prerequisite": "no", - "id": 245, - "instructors": [ - { - "label": "Author", - "visibility": "visible", - "id": 1, - "description": "", - "email": "admin@llms.test", - "first_name": "arst\" onfocus=\"alert(1)\"", - "last_name": "arst" - } - ], - "length": "", - "menu_order": 0, - "modified": "2020-08-05 19:40:11", - "modified_gmt": "2020-08-05 19:40:11", - "name": "restrictions-testing", - "password": "", - "permalink": "https://llms.test/course/restrictions-testing/", - "ping_status": "closed", - "prerequisite": 0, - "prerequisite_track": 0, - "sales_page_content_page_id": 0, - "sales_page_content_type": "content", - "sales_page_content_url": "", - "sections": [ - { - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 55, - "last_name": "" - }, - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-08-04 23:07:01", - "date_gmt": "2020-08-04 23:07:01", - "excerpt": "", - "id": 247, - "lessons": [ - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 55, - "last_name": "" - }, - "av_prog_auto_advance": "course", - "av_prog_auto_play": "course", - "av_prog_require_completion": "course", - "av_vimeo_player_disable_controls": "course", - "av_vimeo_player_disable_speed": "course", - "comment_status": "closed", - "content": "", - "custom": { - "_llms_generated_from_id": [ - "88" - ], - "_wp_old_slug": [ - "new-lesson" - ] - }, - "date": "2020-08-04 23:07:01", - "date_available": "", - "date_gmt": "2020-08-04 23:07:01", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "no", - "id": 248, - "menu_order": 0, - "modified": "2020-08-05 18:34:15", - "modified_gmt": "2020-08-05 18:34:15", - "name": "regular", - "order": 1, - "parent_course": 245, - "parent_section": 247, - "password": "", - "permalink": "https://llms.test/lesson/regular/", - "ping_status": "closed", - "points": 1, - "prerequisite": 0, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "Regular", - "type": "lesson", - "video_embed": "" - }, - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 55, - "last_name": "" - }, - "av_prog_auto_advance": "course", - "av_prog_auto_play": "course", - "av_prog_require_completion": "course", - "av_vimeo_player_disable_controls": "course", - "av_vimeo_player_disable_speed": "course", - "comment_status": "closed", - "content": "", - "custom": { - "_llms_generated_from_id": [ - "90" - ], - "_wp_old_slug": [ - "new-lesson-2" - ] - }, - "date": "2020-08-04 23:07:01", - "date_available": "", - "date_gmt": "2020-08-04 23:07:01", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "yes", - "id": 250, - "menu_order": 0, - "modified": "2020-08-05 18:34:15", - "modified_gmt": "2020-08-05 18:34:15", - "name": "has-prereq", - "order": 2, - "parent_course": 245, - "parent_section": 247, - "password": "", - "permalink": "https://llms.test/lesson/has-prereq/", - "ping_status": "closed", - "points": 1, - "prerequisite": 248, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "Has Prereq", - "type": "lesson", - "video_embed": "" - }, - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 55, - "last_name": "" - }, - "av_prog_auto_advance": "course", - "av_prog_auto_play": "course", - "av_prog_require_completion": "course", - "av_vimeo_player_disable_controls": "course", - "av_vimeo_player_disable_speed": "course", - "comment_status": "closed", - "content": "", - "custom": { - "_llms_generated_from_id": [ - "93" - ] - }, - "date": "2020-08-04 23:07:48", - "date_available": "", - "date_gmt": "2020-08-04 23:07:48", - "days_before_available": 5, - "drip_method": "enrollment", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "no", - "id": 252, - "menu_order": 0, - "modified": "2020-08-05 18:34:15", - "modified_gmt": "2020-08-05 18:34:15", - "name": "has-drip", - "order": 3, - "parent_course": 245, - "parent_section": 247, - "password": "", - "permalink": "https://llms.test/lesson/has-drip/", - "ping_status": "closed", - "points": 1, - "prerequisite": 0, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "Has Drip", - "type": "lesson", - "video_embed": "" - }, - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "author": { - "description": "", - "email": "admin@wpllms.test", - "first_name": "", - "id": 55, - "last_name": "" - }, - "av_prog_auto_advance": "course", - "av_prog_auto_play": "course", - "av_prog_require_completion": "course", - "av_vimeo_player_disable_controls": "course", - "av_vimeo_player_disable_speed": "course", - "comment_status": "closed", - "content": "<!-- wp:html -->\n<strong id=\"free-lesson-content\">Free lesson content.</strong>\n<!-- /wp:html -->", - "custom": { - "_llms_generated_from_id": [ - "95" - ] - }, - "date": "2020-08-04 23:07:48", - "date_available": "", - "date_gmt": "2020-08-04 23:07:48", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "yes", - "has_prerequisite": "no", - "id": 254, - "menu_order": 0, - "modified": "2020-08-05 18:34:15", - "modified_gmt": "2020-08-05 18:34:15", - "name": "is-free", - "order": 4, - "parent_course": 245, - "parent_section": 247, - "password": "", - "permalink": "https://llms.test/lesson/is-free/", - "ping_status": "closed", - "points": 1, - "prerequisite": 0, - "quiz": 0, - "quiz_enabled": "no", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "Is Free", - "type": "lesson", - "video_embed": "" - }, - { - "assignment": 0, - "assignment_enabled": "no", - "audio_embed": "", - "author": { - "description": "", - "email": "admin@llms.test", - "first_name": "arst\" onfocus=\"alert(1)\"", - "id": 1, - "last_name": "arst" - }, - "av_prog_auto_advance": "course", - "av_prog_auto_play": "course", - "av_prog_require_completion": "course", - "av_vimeo_player_disable_controls": "course", - "av_vimeo_player_disable_speed": "course", - "comment_status": "closed", - "content": "", - "custom": { - "_wp_old_slug": [ - "new-lesson-5" - ] - }, - "date": "2020-08-05 19:30:37", - "date_available": "", - "date_gmt": "2020-08-05 19:30:37", - "days_before_available": 0, - "drip_method": "", - "excerpt": "", - "featured_image": "", - "free_lesson": "no", - "has_prerequisite": "no", - "id": 311, - "menu_order": 0, - "modified": "2020-08-05 19:32:54", - "modified_gmt": "2020-08-05 19:32:54", - "name": "has-quiz", - "order": 5, - "parent_course": 245, - "parent_section": 247, - "password": "", - "permalink": "https://llms.test/lesson/has-quiz/", - "ping_status": "closed", - "points": 1, - "prerequisite": 0, - "quiz": { - "allowed_attempts": 5, - "author": { - "description": "", - "email": "admin@llms.test", - "first_name": "arst\" onfocus=\"alert(1)\"", - "id": 1, - "last_name": "arst" - }, - "comment_status": "closed", - "content": "", - "custom": [], - "date": "2020-08-05 19:30:38", - "date_gmt": "2020-08-05 19:30:38", - "excerpt": "", - "id": 313, - "lesson_id": 311, - "limit_attempts": "no", - "limit_time": "no", - "menu_order": 0, - "modified": "2020-08-05 19:30:38", - "modified_gmt": "2020-08-05 19:30:38", - "name": "313", - "passing_percent": 65, - "password": "", - "permalink": "https://llms.test/quiz/313/", - "ping_status": "closed", - "points": 1, - "questions": [ - { - "choices": [ - { - "id": "5f2b08de35a8e", - "choice": "11", - "choice_type": "text", - "correct": false, - "marker": "A", - "question_id": "314", - "type": "choice" - }, - { - "id": "5f2b08de398ee", - "choice": "2", - "choice_type": "text", - "correct": true, - "marker": "B", - "question_id": "314", - "type": "choice" - } - ], - "clarifications": "", - "clarifications_enabled": "no", - "comment_status": "closed", - "content": "", - "date_gmt": "2020-08-05 19:30:38", - "description_enabled": "no", - "id": 314, - "image": [], - "menu_order": 1, - "modified_gmt": "2020-08-05 19:31:04", - "multi_choices": "no", - "name": "314", - "parent_id": 313, - "password": "", - "ping_status": "closed", - "points": 1, - "question": "", - "question_type": "choice", - "title": "1 + 1 = ?", - "type": "llms_question", - "video_enabled": "no", - "video_src": "" - } - ], - "random_questions": "no", - "show_correct_answer": "no", - "status": "publish", - "time_limit": 30, - "title": "New Lesson Quiz", - "type": "llms_quiz" - }, - "quiz_enabled": "yes", - "require_assignment_passing_grade": "yes", - "require_passing_grade": "yes", - "status": "publish", - "time_available": "", - "title": "Has Quiz", - "type": "lesson", - "video_embed": "" - } - ], - "menu_order": 0, - "modified": "2020-08-04 23:07:01", - "modified_gmt": "2020-08-04 23:07:01", - "name": "new-section-2", - "order": 1, - "parent_course": 245, - "password": "", - "ping_status": "closed", - "status": "publish", - "title": "New Section", - "type": "section" - } - ], - "start_date": "", - "status": "publish", - "tags": [], - "temp_calc_data": [], - "tile_featured_video": "no", - "time_period": "no", - "title": "Restrictions Testing", - "tracks": [], - "type": "course", - "video_embed": "" -} diff --git a/tests/assets/klim-musalimov-rDMacl1FDjw-unsplash.jpeg b/tests/assets/klim-musalimov-rDMacl1FDjw-unsplash.jpeg deleted file mode 100644 index aee5f22d17..0000000000 Binary files a/tests/assets/klim-musalimov-rDMacl1FDjw-unsplash.jpeg and /dev/null differ diff --git a/tests/assets/lifterlms-en_US-cd71ad734c92669051f6fd28eb90dfd4.json b/tests/assets/lifterlms-en_US-cd71ad734c92669051f6fd28eb90dfd4.json deleted file mode 100644 index 57d7834473..0000000000 --- a/tests/assets/lifterlms-en_US-cd71ad734c92669051f6fd28eb90dfd4.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "translation-revision-date": "2020-11-20T23:21:55.271Z", - "generator": "llms-dev 1.0.0\n", - "source": "assets/js/llms-test-messages.js", - "domain": "messages", - "locale_data": { - "messages": { - "": { - "domain": "messages", - "lang": "en", - "plural-forms": "nplurals=2; plural=(n != 1);" - }, - "LifterLMS": [ - "MyLMS" - ], - "Course": [ - "Module" - ], - } - } -} diff --git a/tests/assets/lifterlms-en_US.mo b/tests/assets/lifterlms-en_US.mo deleted file mode 100644 index 2e92f342bb..0000000000 Binary files a/tests/assets/lifterlms-en_US.mo and /dev/null differ diff --git a/tests/assets/lifterlms-en_US.po b/tests/assets/lifterlms-en_US.po deleted file mode 100644 index 4510657c18..0000000000 --- a/tests/assets/lifterlms-en_US.po +++ /dev/null @@ -1,10969 +0,0 @@ -# Copyright (C) 2018 lifterlms -# This file is distributed under the same license as the lifterlms package. -msgid "" -msgstr "" -"Project-Id-Version: lifterlms\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language-Team: LifterLMS <help@lifterlms.com>\n" -"Report-Msgid-Bugs-To: https://github.com/gocodebox/lifterlms/issues\n" -"X-Poedit-Basepath: ..\n" -"X-Poedit-KeywordsList: __;_e;_ex:1,2c;_n:1,2;_n_noop:1,2;_nx:1,2,4c;" -"_nx_noop:1,2,3c;_x:1,2c;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;" -"esc_html_e;esc_html_x:1,2c\n" -"X-Poedit-SourceCharset: UTF-8\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"POT-Creation-Date: \n" -"PO-Revision-Date: \n" -"X-Generator: Poedit 1.8.7\n" -"Last-Translator: \n" -"Language: en_US\n" -"X-Poedit-SearchPath-0: .\n" -"X-Poedit-SearchPathExcluded-0: *.js\n" - -#: lifterlms.php:497, includes/admin/class.llms.admin.menus.php:131 -msgid "Settings" -msgstr "Options" - -#: includes/class.llms.ajax.handler.php:22, -#: includes/class.llms.ajax.handler.php:41, -#: includes/class.llms.ajax.handler.php:778 -msgid "Missing required parameters" -msgstr "" - -#: includes/class.llms.ajax.handler.php:28 -msgid "Members are being enrolled in the background. You may leave this page." -msgstr "" - -#: includes/class.llms.ajax.handler.php:69 -msgid "There was a problem deleting your access plan, please try again." -msgstr "" - -#: includes/class.llms.ajax.handler.php:97 -msgid "The export is being generated and will be emailed to %s when complete." -msgstr "" - -#: includes/class.llms.ajax.handler.php:155 -msgid "Missing required parameters" -msgstr "" - -#: includes/class.llms.ajax.handler.php:260 -msgid "Missing required parameters." -msgstr "" - -#: includes/class.llms.ajax.handler.php:266 -msgid "There was an error removing the course, please try again." -msgstr "" - -#: includes/class.llms.ajax.handler.php:491, -#: includes/class.llms.ajax.handler.php:549, -#: includes/class.llms.ajax.handler.php:615, -#: includes/class.llms.template.loader.php:345 -msgid "You must be logged in to take quizzes." -msgstr "" - -#: includes/class.llms.ajax.handler.php:503 -msgid "" -"There was an error starting the quiz. Please return to the lesson and begin " -"again." -msgstr "" - -#: includes/class.llms.ajax.handler.php:513 -msgid "Unable to start quiz because the quiz does not contain any questions." -msgstr "" - -#: includes/class.llms.ajax.handler.php:556, -#: includes/class.llms.ajax.handler.php:620 -msgid "Missing required parameters. Could not proceed." -msgstr "" - -#: includes/class.llms.ajax.handler.php:568 -msgid "" -"There was an error recording your answer the quiz. Please return to the " -"lesson and begin again." -msgstr "" - -#: includes/class.llms.ajax.handler.php:738, -#: includes/llms.functions.core.php:841, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:48, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:54 -msgid "ID#" -msgstr "" - -#: includes/class.llms.ajax.handler.php:782 -msgid "Invalid status" -msgstr "" - -#: includes/class.llms.ajax.handler.php:806 -msgid "Please enter a coupon code." -msgstr "" - -#: includes/class.llms.ajax.handler.php:811 -msgid "Please enter a plan ID." -msgstr "" - -#: includes/class.llms.ajax.handler.php:820, -#: includes/controllers/class.llms.controller.orders.php:211 -msgid "Coupon code \"%s\" not found." -msgstr "" - -#: includes/class.llms.ajax.php:630, includes/class.llms.ajax.php:759, -#: includes/class.llms.lesson.handler.php:31, -#: includes/class.llms.post.handler.php:195 -msgid "unassigned" -msgstr "" - -#: includes/class.llms.certificates.php:85 -msgid "Unable to open export file (HTML certificate) for writing." -msgstr "" - -#: includes/class.llms.certificates.php:89 -msgid "Unable to write to export file (HTML certificate)." -msgstr "" - -#. translators: %1$s = url-safe certificate title, %2$s = random alpha-numeric characters for filename obscurity -#: includes/class.llms.certificates.php:118 -msgctxt "certificate download filename" -msgid "certificate-%1$s-%2$s" -msgstr "" - -#: includes/class.llms.date.php:158 -msgctxt "Localized Order DateTime" -msgid "%1$b %2$d, %3$Y @ %4$I:%5$M %6$p" -msgstr "" - -#: includes/class.llms.date.php:178, includes/llms.functions.core.php:260 -msgid "hours" -msgstr "" - -#: includes/class.llms.date.php:180, includes/llms.functions.core.php:252 -msgid "hour" -msgstr "" - -#: includes/class.llms.date.php:183 -msgid "%1$d %2$s " -msgstr "" - -#: includes/class.llms.date.php:187, includes/llms.functions.core.php:262 -msgid "seconds" -msgstr "" - -#: includes/class.llms.date.php:189, includes/llms.functions.core.php:254 -msgid "second" -msgstr "" - -#: includes/class.llms.date.php:200, includes/llms.functions.core.php:261 -msgid "minutes" -msgstr "" - -#: includes/class.llms.date.php:202, includes/llms.functions.core.php:253 -msgid "minute" -msgstr "" - -#: includes/class.llms.gateway.manual.php:20 -msgid "" -"Collect manual or offline payments. Also handles any free orders during " -"checkout." -msgstr "" - -#: includes/class.llms.gateway.manual.php:21, -#: includes/class.llms.gateway.manual.php:22 -msgid "Manual" -msgstr "" - -#: includes/class.llms.gateway.manual.php:23 -msgid "Pay manually via check" -msgstr "" - -#: includes/class.llms.gateway.manual.php:71, -#: includes/class.llms.gateway.manual.php:95 -msgid "Payment Instructions" -msgstr "" - -#: includes/class.llms.gateway.manual.php:94 -msgid "" -"Displayed to the user when this gateway is selected during checkout. Add " -"information here instructing the student on how to send payment." -msgstr "" - -#: includes/class.llms.gateway.manual.php:128 -msgid "Payment method switched from \"%1$s\" to \"%2$s\"" -msgstr "" - -#: includes/class.llms.gateway.manual.php:162, -#: includes/functions/llms.functions.updates.php:214 -msgid "Free" -msgstr "" - -#: includes/class.llms.generator.php:192 -msgid "No generator supplied." -msgstr "" - -#: includes/class.llms.generator.php:208, includes/class.llms.l10n.js.php:196, -#: includes/class.llms.l10n.js.php:305, -#: includes/admin/post-types/class.llms.post.tables.php:44 -msgid "Clone" -msgstr "" - -#: includes/class.llms.generator.php:250 -msgid "Missing required \"courses\" array" -msgstr "" - -#: includes/class.llms.generator.php:252 -msgid "\"courses\" must be an array" -msgstr "" - -#: includes/class.llms.generator.php:341 -msgid "Error creating course" -msgstr "" - -#: includes/class.llms.generator.php:427 -msgid "Error creating lesson" -msgstr "" - -#: includes/class.llms.generator.php:501 -msgid "Error creating quiz" -msgstr "" - -#: includes/class.llms.generator.php:547 -msgid "Error creating question" -msgstr "" - -#: includes/class.llms.generator.php:599 -msgid "Error creating section" -msgstr "" - -#: includes/class.llms.generator.php:844 -msgid "Error creating new term \"%s\"" -msgstr "" - -#: includes/class.llms.generator.php:1029 -msgid "The supplied file cannot be processed by the importer." -msgstr "" - -#: includes/class.llms.generator.php:1042 -msgid "Invalid generator supplied" -msgstr "" - -#: includes/class.llms.install.php:224, -#: includes/admin/class.llms.admin.setup.wizard.php:322, -#: includes/admin/settings/class.llms.settings.courses.php:84 -msgid "Course Catalog" -msgstr "" - -#: includes/class.llms.install.php:230, -#: includes/admin/class.llms.admin.setup.wizard.php:326 -msgid "Membership Catalog" -msgstr "" - -#: includes/class.llms.install.php:236 -msgid "Purchase" -msgstr "" - -#: includes/class.llms.install.php:242, -#: includes/class.llms.student.dashboard.php:135, -#: includes/functions/llms.functions.templates.dashboard.php:257 -msgid "My Courses" -msgstr "" - -#: includes/class.llms.install.php:343 -msgctxt "course difficulty name" -msgid "Beginner" -msgstr "" - -#: includes/class.llms.install.php:344 -msgctxt "course difficulty name" -msgid "Intermediate" -msgstr "" - -#: includes/class.llms.install.php:345 -msgctxt "course difficulty name" -msgid "Advanced" -msgstr "" - -#: includes/class.llms.install.php:553, includes/class.llms.install.php:573, -#: includes/admin/class.llms.admin.notices.core.php:67, -#: includes/admin/class.llms.admin.notices.php:195, -#: includes/admin/class.llms.admin.page.status.php:21, -#: includes/admin/class.llms.admin.page.status.php:184 -msgid "Action failed. Please refresh the page and retry." -msgstr "" - -#: includes/class.llms.install.php:557, includes/class.llms.install.php:577, -#: includes/admin/class.llms.admin.notices.core.php:70, -#: includes/admin/class.llms.admin.notices.php:198 -msgid "Cheatin’ huh?" -msgstr "" - -#: includes/class.llms.install.php:629 -msgid "The LifterLMS database update is complete." -msgstr "" - -#: includes/class.llms.l10n.js.php:41, includes/class.llms.l10n.js.php:378 -msgid "This is a %2$s %1$s String" -msgstr "" - -#: includes/class.llms.l10n.js.php:48, includes/class.llms.l10n.js.php:379, -#: includes/functions/llms.functions.access.php:177 -msgid "You do not have permission to access this content" -msgstr "" - -#: includes/class.llms.l10n.js.php:55, includes/class.llms.l10n.js.php:380 -msgid "There is an issue with your chosen password." -msgstr "" - -#: includes/class.llms.l10n.js.php:56, includes/class.llms.l10n.js.php:381 -msgid "Too Short" -msgstr "" - -#: includes/class.llms.l10n.js.php:57, includes/class.llms.l10n.js.php:382 -msgid "Very Weak" -msgstr "" - -#: includes/class.llms.l10n.js.php:58, includes/class.llms.l10n.js.php:383 -msgid "Weak" -msgstr "" - -#: includes/class.llms.l10n.js.php:59, includes/class.llms.l10n.js.php:384 -msgid "Medium" -msgstr "" - -#: includes/class.llms.l10n.js.php:60, includes/class.llms.l10n.js.php:385 -msgid "Strong" -msgstr "" - -#: includes/class.llms.l10n.js.php:61, includes/class.llms.l10n.js.php:386 -msgid "Mismatch" -msgstr "" - -#: includes/class.llms.l10n.js.php:68, includes/class.llms.l10n.js.php:387 -msgid "Members Only Pricing" -msgstr "" - -#: includes/class.llms.l10n.js.php:75, includes/class.llms.l10n.js.php:388 -msgid "Are you sure you want to cancel your subscription?" -msgstr "" - -#: includes/class.llms.l10n.js.php:82, includes/class.llms.l10n.js.php:287, -#: includes/class.llms.post-types.php:316, -#: includes/admin/class.llms.admin.builder.php:773, -#: includes/models/model.llms.lesson.php:347, -#: includes/admin/views/builder/elements.php:21 -msgid "New Lesson" -msgstr "" - -#: includes/class.llms.l10n.js.php:83, includes/class.llms.l10n.js.php:288 -msgid "lessons" -msgstr "" - -#: includes/class.llms.l10n.js.php:84, includes/class.llms.l10n.js.php:289 -msgid "lesson" -msgstr "" - -#: includes/class.llms.l10n.js.php:85, includes/class.llms.l10n.js.php:290, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:140 -msgid "Section %1$d: %2$s" -msgstr "" - -#: includes/class.llms.l10n.js.php:86, includes/class.llms.l10n.js.php:291 -msgid "Lesson %1$d: %2$s" -msgstr "" - -#: includes/class.llms.l10n.js.php:87, includes/class.llms.l10n.js.php:292 -msgid "%1$s Quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:94, includes/class.llms.l10n.js.php:269, -#: includes/class.llms.post-types.php:353 -msgid "New Quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:95, includes/class.llms.l10n.js.php:270 -msgid "quizzes" -msgstr "" - -#: includes/class.llms.l10n.js.php:96, includes/class.llms.l10n.js.php:271, -#: includes/models/model.llms.user.postmeta.php:82 -msgid "quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:103, includes/class.llms.l10n.js.php:293, -#: includes/class.llms.post-types.php:284 -msgid "New Section" -msgstr "" - -#: includes/class.llms.l10n.js.php:104, includes/class.llms.l10n.js.php:294 -msgid "sections" -msgstr "" - -#: includes/class.llms.l10n.js.php:105, includes/class.llms.l10n.js.php:295 -msgid "section" -msgstr "" - -#: includes/class.llms.l10n.js.php:112, includes/class.llms.l10n.js.php:134, -#: includes/class.llms.l10n.js.php:257, -#: includes/admin/settings/class.llms.settings.general.php:109 -msgid "General Settings" -msgstr "" - -#: includes/class.llms.l10n.js.php:113, includes/class.llms.l10n.js.php:272 -msgid "Video Embed URL" -msgstr "" - -#: includes/class.llms.l10n.js.php:114, includes/class.llms.l10n.js.php:273 -msgid "Audio Embed URL" -msgstr "" - -#: includes/class.llms.l10n.js.php:115, includes/class.llms.l10n.js.php:274, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:82, -#: includes/admin/views/builder/lesson.php:127 -msgid "Free Lesson" -msgstr "" - -#: includes/class.llms.l10n.js.php:116, includes/class.llms.l10n.js.php:275 -msgid "Require Passing Grade on Quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:117, includes/class.llms.l10n.js.php:276 -msgid "Require Passing Grade on Assignment" -msgstr "" - -#: includes/class.llms.l10n.js.php:118, includes/class.llms.l10n.js.php:277, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:42 -msgid "Prerequisite" -msgstr "" - -#: includes/class.llms.l10n.js.php:119, includes/class.llms.l10n.js.php:278 -msgid "Drip Method" -msgstr "" - -#: includes/class.llms.l10n.js.php:120, includes/class.llms.l10n.js.php:279, -#: includes/admin/class.llms.admin.settings.php:631 -msgid "None" -msgstr "" - -#: includes/class.llms.l10n.js.php:121, includes/class.llms.l10n.js.php:280, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:44 -msgid "On a specific date" -msgstr "" - -#: includes/class.llms.l10n.js.php:122, includes/class.llms.l10n.js.php:281 -msgid "# of days after course enrollment" -msgstr "" - -#: includes/class.llms.l10n.js.php:123, includes/class.llms.l10n.js.php:282 -msgid "# of days after course start date" -msgstr "" - -#: includes/class.llms.l10n.js.php:124, includes/class.llms.l10n.js.php:283 -msgid "# of days after prerequisite lesson completion" -msgstr "" - -#: includes/class.llms.l10n.js.php:125, includes/class.llms.l10n.js.php:284 -msgid "# of days" -msgstr "" - -#: includes/class.llms.l10n.js.php:126, includes/class.llms.l10n.js.php:250, -#: includes/class.llms.l10n.js.php:285, templates/myaccount/my-orders.php:20, -#: templates/myaccount/my-orders.php:33, -#: templates/myaccount/view-order-transactions.php:18, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:39, -#: templates/admin/post-types/order-transactions.php:19, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:44, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:47 -msgid "Date" -msgstr "" - -#: includes/class.llms.l10n.js.php:127, includes/class.llms.l10n.js.php:286 -msgid "Time" -msgstr "" - -#: includes/class.llms.l10n.js.php:135, includes/class.llms.l10n.js.php:258, -#: includes/abstracts/abstract.llms.payment.gateway.php:278, -#: includes/privacy/class-llms-privacy-exporters.php:102, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:200, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:204, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:35, -#: includes/admin/views/builder/question.php:58 -msgid "Description" -msgstr "" - -#: includes/class.llms.l10n.js.php:136, includes/class.llms.l10n.js.php:259 -msgid "Passing Percentage" -msgstr "" - -#: includes/class.llms.l10n.js.php:137, includes/class.llms.l10n.js.php:260 -msgid "Minimum percentage of total points required to pass the quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:138, includes/class.llms.l10n.js.php:261 -msgid "Limit Attempts" -msgstr "" - -#: includes/class.llms.l10n.js.php:139, includes/class.llms.l10n.js.php:262 -msgid "Limit the maximum number of times a student can take this quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:140, includes/class.llms.l10n.js.php:263 -msgid "Time Limit" -msgstr "" - -#: includes/class.llms.l10n.js.php:141, includes/class.llms.l10n.js.php:264 -msgid "Enforce a maximum number of minutes a student can spend on each attempt" -msgstr "" - -#: includes/class.llms.l10n.js.php:142, includes/class.llms.l10n.js.php:265 -msgid "Show Correct Answers" -msgstr "" - -#: includes/class.llms.l10n.js.php:143, includes/class.llms.l10n.js.php:266 -msgid "" -"When enabled, students will be shown the correct answer to any question they " -"answered incorrectly." -msgstr "" - -#: includes/class.llms.l10n.js.php:144, includes/class.llms.l10n.js.php:267 -msgid "Randomize Question Order" -msgstr "" - -#: includes/class.llms.l10n.js.php:145, includes/class.llms.l10n.js.php:268 -msgid "" -"Display questions in a random order for each attempt. Content questions are " -"locked into their defined positions." -msgstr "" - -#: includes/class.llms.l10n.js.php:152, includes/class.llms.l10n.js.php:296 -msgid "Are you sure you want to detach this %s?" -msgstr "" - -#: includes/class.llms.l10n.js.php:159, includes/class.llms.l10n.js.php:297 -msgid "Select an image" -msgstr "" - -#: includes/class.llms.l10n.js.php:160, includes/class.llms.l10n.js.php:298 -msgid "Use this image" -msgstr "" - -#: includes/class.llms.l10n.js.php:167, includes/class.llms.l10n.js.php:299 -msgid "Are you sure you want to move this %s to the trash?" -msgstr "" - -#: includes/class.llms.l10n.js.php:174, includes/class.llms.l10n.js.php:312 -msgid "%1$s Assignment" -msgstr "" - -#: includes/class.llms.l10n.js.php:175, includes/class.llms.l10n.js.php:313, -#: includes/admin/views/builder/assignment.php:25 -msgid "Add Existing Assignment" -msgstr "" - -#: includes/class.llms.l10n.js.php:176, includes/class.llms.l10n.js.php:314 -msgid "Search for existing assignments..." -msgstr "" - -#: includes/class.llms.l10n.js.php:177, includes/class.llms.l10n.js.php:315 -msgid "Get Your Students Taking Action" -msgstr "" - -#: includes/class.llms.l10n.js.php:178, includes/class.llms.l10n.js.php:316 -msgid "Get Assignments Now!" -msgstr "" - -#: includes/class.llms.l10n.js.php:179, includes/class.llms.l10n.js.php:317 -msgid "Unlock LifterLMS Assignments" -msgstr "" - -#: includes/class.llms.l10n.js.php:186, includes/class.llms.l10n.js.php:318 -msgid "Add Existing Lesson" -msgstr "" - -#: includes/class.llms.l10n.js.php:187, includes/class.llms.l10n.js.php:319 -msgid "Search for existing lessons..." -msgstr "" - -#: includes/class.llms.l10n.js.php:194, includes/class.llms.l10n.js.php:303 -msgid "Searching..." -msgstr "" - -#: includes/class.llms.l10n.js.php:195, includes/class.llms.l10n.js.php:304 -msgid "Attach" -msgstr "" - -#: includes/class.llms.l10n.js.php:197, includes/class.llms.l10n.js.php:306, -#: templates/admin/post-types/order-transactions.php:17, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:347, -#: includes/admin/reporting/tables/llms.table.achievements.php:151, -#: includes/admin/reporting/tables/llms.table.certificates.php:155, -#: includes/admin/reporting/tables/llms.table.course.students.php:368, -#: includes/admin/reporting/tables/llms.table.courses.php:271, -#: includes/admin/reporting/tables/llms.table.questions.php:89, -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:247, -#: includes/admin/reporting/tables/llms.table.quizzes.php:295, -#: includes/admin/reporting/tables/llms.table.student.course.php:182, -#: includes/admin/reporting/tables/llms.table.student.courses.php:196, -#: includes/admin/reporting/tables/llms.table.student.memberships.php:104, -#: includes/admin/reporting/tables/llms.table.students.php:395 -msgid "ID" -msgstr "" - -#: includes/class.llms.l10n.js.php:204, includes/class.llms.l10n.js.php:307 -msgid "Are you sure you want to delete this question?" -msgstr "" - -#: includes/class.llms.l10n.js.php:211, includes/class.llms.l10n.js.php:308 -msgid "" -"An error occurred while trying to load the questions. Please refresh the " -"page and try again." -msgstr "" - -#: includes/class.llms.l10n.js.php:212, includes/class.llms.l10n.js.php:309, -#: includes/admin/views/builder/quiz.php:24 -msgid "Add Existing Quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:213, includes/class.llms.l10n.js.php:310 -msgid "Search for existing quizzes..." -msgstr "" - -#: includes/class.llms.l10n.js.php:214, includes/class.llms.l10n.js.php:311 -msgid "Add a Question" -msgstr "" - -#: includes/class.llms.l10n.js.php:221, includes/class.llms.l10n.js.php:300 -msgid "Use SoundCloud or Spotify audio URLS." -msgstr "" - -#: includes/class.llms.l10n.js.php:222, includes/class.llms.l10n.js.php:301 -msgid "Permalink" -msgstr "" - -#: includes/class.llms.l10n.js.php:223, includes/class.llms.l10n.js.php:302, -#: includes/admin/views/builder/question.php:112 -msgid "Use YouTube, Vimeo, or Wistia video URLS." -msgstr "" - -#: includes/class.llms.l10n.js.php:230 -msgid "Select an Image" -msgstr "" - -#: includes/class.llms.l10n.js.php:231 -msgid "Select Image" -msgstr "" - -#: includes/class.llms.l10n.js.php:238 -msgid "Select a Course/Membership" -msgstr "" - -#: includes/class.llms.l10n.js.php:239 -msgid "Select a student" -msgstr "" - -#: includes/class.llms.l10n.js.php:246 -msgid "Filter by Student(s)" -msgstr "" - -#: includes/class.llms.l10n.js.php:247 -msgid "Error" -msgstr "" - -#: includes/class.llms.l10n.js.php:248 -msgid "Request timed out" -msgstr "" - -#: includes/class.llms.l10n.js.php:249 -msgid "Retry" -msgstr "" - -#: includes/class.llms.l10n.js.php:326 -msgid "There was an error loading the necessary resources. Please try again." -msgstr "" - -#: includes/class.llms.l10n.js.php:333 -msgid "Please select a student to enroll" -msgstr "" - -#: includes/class.llms.l10n.js.php:340, includes/class.llms.l10n.js.php:395 -msgid "Are you sure you want to delete this row? This cannot be undone." -msgstr "" - -#: includes/class.llms.l10n.js.php:341, includes/class.llms.l10n.js.php:402 -msgid "" -"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!" -msgstr "" - -#: includes/class.llms.l10n.js.php:342, includes/class.llms.l10n.js.php:403, -#: includes/class.llms.person.handler.php:379, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:62 -msgid "Cancel" -msgstr "" - -#: includes/class.llms.l10n.js.php:343, includes/class.llms.l10n.js.php:404, -#: templates/admin/post-types/order-transactions.php:76 -msgid "Refund" -msgstr "" - -#: includes/class.llms.l10n.js.php:344, includes/class.llms.l10n.js.php:405, -#: templates/admin/post-types/order-transactions.php:100 -msgid "Record a Manual Payment" -msgstr "" - -#: includes/class.llms.l10n.js.php:345, includes/class.llms.l10n.js.php:406 -msgid "Copy this code and paste it into the desired area" -msgstr "" - -#: includes/class.llms.l10n.js.php:346, includes/class.llms.l10n.js.php:407, -#: templates/myaccount/my-orders.php:49, -#: includes/admin/reporting/tables/llms.table.certificates.php:35 -msgid "View" -msgstr "" - -#: includes/class.llms.l10n.js.php:353 -msgid "Remarks to Student" -msgstr "" - -#: includes/class.llms.l10n.js.php:354, -#: includes/admin/views/builder/question.php:43 -msgid "points" -msgstr "" - -#: includes/class.llms.l10n.js.php:361 -msgid "Are you sure you wish to quit this quiz attempt?" -msgstr "" - -#: includes/class.llms.l10n.js.php:362 -msgid "Grading Quiz..." -msgstr "" - -#: includes/class.llms.l10n.js.php:363 -msgid "Loading Question..." -msgstr "" - -#: includes/class.llms.l10n.js.php:364 -msgid "An unknown error occurred. Please try again." -msgstr "" - -#: includes/class.llms.l10n.js.php:365 -msgid "Loading Quiz..." -msgstr "" - -#: includes/class.llms.l10n.js.php:366 -msgid "Time Remaining" -msgstr "" - -#: includes/class.llms.l10n.js.php:367 -msgid "Next Question" -msgstr "" - -#: includes/class.llms.l10n.js.php:368 -msgid "Complete Quiz" -msgstr "" - -#: includes/class.llms.l10n.js.php:369 -msgid "Previous Question" -msgstr "" - -#: includes/class.llms.l10n.js.php:370 -msgid "Loading..." -msgstr "" - -#: includes/class.llms.l10n.js.php:371 -msgid "You must select an answer to continue." -msgstr "" - -#: includes/class.llms.nav.menus.php:41, -#: includes/privacy/class-llms-privacy.php:19 -msgid "LifterLMS" -msgstr "MyLMS" - -#: includes/class.llms.nav.menus.php:68 -msgid "Custom Link" -msgstr "" - -#: includes/class.llms.nav.menus.php:87 -msgctxt "customizer menu section title" -msgid "LifterLMS" -msgstr "" - -#: includes/class.llms.nav.menus.php:158, includes/class.llms.nav.menus.php:159 -msgid "Sign In" -msgstr "" - -#: includes/class.llms.nav.menus.php:163, -#: includes/class.llms.nav.menus.php:164, -#: includes/class.llms.student.dashboard.php:181 -msgid "Sign Out" -msgstr "" - -#: includes/class.llms.person.handler.php:84 -msgid "Username" -msgstr "" - -#: includes/class.llms.person.handler.php:98, -#: includes/class.llms.person.handler.php:265, -#: includes/class.llms.person.handler.php:419 -msgid "Email Address" -msgstr "" - -#: includes/class.llms.person.handler.php:108 -msgid "Confirm Email Address" -msgstr "" - -#: includes/class.llms.person.handler.php:127, -#: includes/admin/reporting/tables/llms.table.course.students.php:382, -#: includes/admin/reporting/tables/llms.table.students.php:414 -msgid "First Name" -msgstr "" - -#: includes/class.llms.person.handler.php:135, -#: includes/admin/reporting/tables/llms.table.course.students.php:377, -#: includes/admin/reporting/tables/llms.table.students.php:409 -msgid "Last Name" -msgstr "" - -#: includes/class.llms.person.handler.php:148 -msgid "Street Address" -msgstr "" - -#: includes/class.llms.person.handler.php:158 -msgid "Apartment, suite, or unit" -msgstr "" - -#: includes/class.llms.person.handler.php:165 -msgid "City" -msgstr "" - -#: includes/class.llms.person.handler.php:173 -msgid "State" -msgstr "" - -#: includes/class.llms.person.handler.php:181 -msgid "Zip Code" -msgstr "" - -#: includes/class.llms.person.handler.php:190, -#: includes/admin/settings/class.llms.settings.checkout.php:230 -msgid "Country" -msgstr "" - -#: includes/class.llms.person.handler.php:203, -#: includes/admin/settings/class.llms.settings.accounts.php:355, -#: includes/admin/settings/class.llms.settings.accounts.php:400, -#: includes/admin/settings/class.llms.settings.accounts.php:449 -msgid "Phone Number" -msgstr "" - -#: includes/class.llms.person.handler.php:205 -msgctxt "Phone Number Placeholder" -msgid "(123) 456 - 7890" -msgstr "" - -#: includes/class.llms.person.handler.php:215 -msgid "Have a voucher?" -msgstr "" - -#: includes/class.llms.person.handler.php:226, -#: templates/myaccount/form-redeem-voucher.php:18, -#: templates/myaccount/form-redeem-voucher.php:19 -msgid "Voucher Code" -msgstr "" - -#: includes/class.llms.person.handler.php:265, -#: includes/class.llms.person.handler.php:419 -msgid "Username or Email Address" -msgstr "" - -#: includes/class.llms.person.handler.php:273, -#: includes/class.llms.person.handler.php:336 -msgid "Password" -msgstr "" - -#: includes/class.llms.person.handler.php:282, -#: templates/global/form-login.php:34 -msgid "Login" -msgstr "" - -#: includes/class.llms.person.handler.php:290 -msgid "Remember me" -msgstr "" - -#: includes/class.llms.person.handler.php:299 -msgid "Lost your password?" -msgstr "" - -#: includes/class.llms.person.handler.php:324 -msgid "Current Password" -msgstr "" - -#: includes/class.llms.person.handler.php:336 -msgid "New Password" -msgstr "" - -#: includes/class.llms.person.handler.php:347 -msgid "Confirm New Password" -msgstr "" - -#: includes/class.llms.person.handler.php:347 -msgid "Confirm Password" -msgstr "" - -#: includes/class.llms.person.handler.php:358 -msgid "A %s password is required." -msgstr "" - -#: includes/class.llms.person.handler.php:360 -msgid "A minimum password strength of %s is required." -msgstr "" - -#: includes/class.llms.person.handler.php:366 -msgid "" -"The password must be at least 6 characters in length. Consider adding " -"letters, numbers, and symbols to increase the password strength." -msgstr "" - -#: includes/class.llms.person.handler.php:379 -msgid "Change Password" -msgstr "" - -#: includes/class.llms.person.handler.php:403 -msgid "" -"Lost your password? Enter your email address and we will send you a link to " -"reset it." -msgstr "" - -#: includes/class.llms.person.handler.php:405 -msgid "" -"Lost your password? Enter your username or email address and we will send " -"you a link to reset it." -msgstr "" - -#: includes/class.llms.person.handler.php:428, -#: templates/emails/reset-password.php:14 -msgid "Reset Password" -msgstr "" - -#: includes/class.llms.person.handler.php:443 -msgid "Update Password" -msgstr "" - -#: includes/class.llms.person.handler.php:649 -msgid "" -"Could not find an account with the supplied email address and password " -"combination." -msgstr "" - -#: includes/class.llms.person.handler.php:820 -msgid "No user ID specified." -msgstr "" - -#: includes/class.llms.person.handler.php:919 -msgid "%s is a required field" -msgstr "" - -#: includes/class.llms.person.handler.php:938 -msgid "An account with the email address \"%s\" already exists." -msgstr "" - -#: includes/class.llms.person.handler.php:948 -msgid "The username \"%s\" is invalid, please try a different username." -msgstr "" - -#: includes/class.llms.person.handler.php:952 -msgid "An account with the username \"%s\" already exists." -msgstr "" - -#: includes/class.llms.person.handler.php:965 -msgid "The submitted %s was incorrect." -msgstr "" - -#: includes/class.llms.person.handler.php:978 -msgid "\"%1$s\" is an invalid option for %2$s" -msgstr "" - -#: includes/class.llms.person.handler.php:990 -msgid "%s must be numeric" -msgstr "" - -#: includes/class.llms.person.handler.php:998 -msgid "%s must be a valid email address" -msgstr "" - -#: includes/class.llms.person.handler.php:1018 -msgid "%1$s must match %2$s" -msgstr "" - -#: includes/class.llms.post-types.php:83 -msgctxt "Order status" -msgid "Completed" -msgstr "" - -#: includes/class.llms.post-types.php:84 -msgid "Completed <span class=\"count\">(%s)</span>" -msgid_plural "Completed <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:89 -msgctxt "Order status" -msgid "Active" -msgstr "" - -#: includes/class.llms.post-types.php:90 -msgid "Active <span class=\"count\">(%s)</span>" -msgid_plural "Active <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:93 -msgctxt "Order status" -msgid "Expired" -msgstr "" - -#: includes/class.llms.post-types.php:94 -msgid "Expired <span class=\"count\">(%s)</span>" -msgid_plural "Expired <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:97 -msgctxt "Order status" -msgid "On Hold" -msgstr "" - -#: includes/class.llms.post-types.php:98 -msgid "On Hold <span class=\"count\">(%s)</span>" -msgid_plural "On Hold <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:101 -msgctxt "Order status" -msgid "Pending Cancellation" -msgstr "" - -#: includes/class.llms.post-types.php:102 -msgid "Pending Cancellation <span class=\"count\">(%s)</span>" -msgid_plural "Pending Cancellation <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:107 -msgctxt "Order status" -msgid "Pending Payment" -msgstr "" - -#: includes/class.llms.post-types.php:108 -msgid "Pending Payment <span class=\"count\">(%s)</span>" -msgid_plural "Pending Payment <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:111 -msgctxt "Order status" -msgid "Cancelled" -msgstr "" - -#: includes/class.llms.post-types.php:112 -msgid "Cancelled <span class=\"count\">(%s)</span>" -msgid_plural "Cancelled <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:115 -msgctxt "Order status" -msgid "Refunded" -msgstr "" - -#: includes/class.llms.post-types.php:116, -#: includes/class.llms.post-types.php:947 -msgid "Refunded <span class=\"count\">(%s)</span>" -msgid_plural "Refunded <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:119 -msgctxt "Order status" -msgid "Failed" -msgstr "" - -#: includes/class.llms.post-types.php:120, -#: includes/class.llms.post-types.php:931 -msgid "Failed <span class=\"count\">(%s)</span>" -msgid_plural "Failed <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:239, -#: includes/admin/class.llms.admin.builder.php:38, -#: includes/admin/class.llms.admin.import.php:74, -#: includes/integrations/class.llms.integration.buddypress.php:52, -#: includes/integrations/class.llms.integration.buddypress.php:65, -#: includes/admin/analytics/class.llms.analytics.courses.php:20, -#: includes/admin/analytics/class.llms.analytics.sales.php:103, -#: includes/admin/reporting/class.llms.admin.reporting.php:250, -#: includes/admin/settings/class.llms.settings.courses.php:19, -#: templates/admin/analytics/analytics.php:90, -#: templates/admin/reporting/nav-filters.php:72, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:152, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:51, -#: includes/admin/reporting/tables/llms.table.courses.php:157, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:83, -#: templates/admin/reporting/tabs/courses/course.php:12 -msgid "Courses" -msgstr "" - -#: includes/class.llms.post-types.php:240, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:40, -#: includes/admin/reporting/tables/llms.table.quizzes.php:305, -#: includes/admin/views/builder/editor.php:15 -msgid "Course" -msgstr "" - -#: includes/class.llms.post-types.php:241 -msgctxt "Admin menu name" -msgid "Courses" -msgstr "" - -#: includes/class.llms.post-types.php:242 -msgid "Add Course" -msgstr "" - -#: includes/class.llms.post-types.php:243 -msgid "Add New Course" -msgstr "" - -#: includes/class.llms.post-types.php:244, -#: includes/class.llms.post-types.php:282, -#: includes/class.llms.post-types.php:314, -#: includes/class.llms.post-types.php:351, -#: includes/class.llms.post-types.php:388, -#: includes/class.llms.post-types.php:427, -#: includes/class.llms.post-types.php:470, -#: includes/class.llms.post-types.php:511, -#: includes/class.llms.post-types.php:554, -#: includes/class.llms.post-types.php:596, -#: includes/class.llms.post-types.php:635, -#: includes/class.llms.post-types.php:677, -#: includes/class.llms.post-types.php:719, -#: includes/class.llms.post-types.php:758, -#: includes/class.llms.post-types.php:797, -#: includes/class.llms.post-types.php:837, -#: includes/class.llms.post-types.php:875, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:51 -msgid "Edit" -msgstr "" - -#: includes/class.llms.post-types.php:245 -msgid "Edit Course" -msgstr "" - -#: includes/class.llms.post-types.php:246 -msgid "New Course" -msgstr "" - -#: includes/class.llms.post-types.php:247, -#: includes/class.llms.post-types.php:248, templates/loop/view-link.php:20 -msgid "View Course" -msgstr "" - -#: includes/class.llms.post-types.php:249 -msgid "Search Courses" -msgstr "" - -#: includes/class.llms.post-types.php:250 -msgid "No Courses found" -msgstr "" - -#: includes/class.llms.post-types.php:251 -msgid "No Courses found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:252 -msgid "Parent Course" -msgstr "" - -#: includes/class.llms.post-types.php:254 -msgid "This is where you can add new courses." -msgstr "" - -#: includes/class.llms.post-types.php:264 -msgctxt "course url slug" -msgid "course" -msgstr "" - -#: includes/class.llms.post-types.php:278, -#: includes/admin/class.llms.admin.import.php:78 -msgid "Sections" -msgstr "" - -#: includes/class.llms.post-types.php:279, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:41, -#: includes/admin/views/builder/elements.php:15 -msgid "Section" -msgstr "" - -#: includes/class.llms.post-types.php:280 -msgid "Add Section" -msgstr "" - -#: includes/class.llms.post-types.php:281 -msgid "Add New Section" -msgstr "" - -#: includes/class.llms.post-types.php:283 -msgid "Edit Section" -msgstr "" - -#: includes/class.llms.post-types.php:285, -#: includes/class.llms.post-types.php:286 -msgid "View Section" -msgstr "" - -#: includes/class.llms.post-types.php:287 -msgid "Search Sections" -msgstr "" - -#: includes/class.llms.post-types.php:288 -msgid "No Sections found" -msgstr "" - -#: includes/class.llms.post-types.php:289 -msgid "No Sections found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:290 -msgid "Parent Sections" -msgstr "" - -#: includes/class.llms.post-types.php:291 -msgctxt "Admin menu name" -msgid "Sections" -msgstr "" - -#: includes/class.llms.post-types.php:293 -msgid "This is where sections are stored." -msgstr "" - -#: includes/class.llms.post-types.php:310, -#: includes/admin/class.llms.admin.import.php:82 -msgid "Lessons" -msgstr "" - -#: includes/class.llms.post-types.php:311, -#: includes/admin/reporting/tables/llms.table.quizzes.php:310, -#: includes/admin/views/builder/editor.php:19 -msgid "Lesson" -msgstr "" - -#: includes/class.llms.post-types.php:312 -msgid "Add Lesson" -msgstr "" - -#: includes/class.llms.post-types.php:313 -msgid "Add New Lesson" -msgstr "" - -#: includes/class.llms.post-types.php:315 -msgid "Edit Lesson" -msgstr "" - -#: includes/class.llms.post-types.php:317, -#: includes/class.llms.post-types.php:318 -msgid "View Lesson" -msgstr "" - -#: includes/class.llms.post-types.php:319 -msgid "Search Lessons" -msgstr "" - -#: includes/class.llms.post-types.php:320 -msgid "No Lessons found" -msgstr "" - -#: includes/class.llms.post-types.php:321 -msgid "No Lessons found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:322 -msgid "Parent Lessons" -msgstr "" - -#: includes/class.llms.post-types.php:323 -msgctxt "Admin menu name" -msgid "Lessons" -msgstr "" - -#: includes/class.llms.post-types.php:325 -msgid "This is where you can view all of the lessons." -msgstr "" - -#: includes/class.llms.post-types.php:335 -msgctxt "lesson url slug" -msgid "lesson" -msgstr "" - -#: includes/class.llms.post-types.php:347, -#: includes/admin/class.llms.admin.import.php:90, -#: includes/admin/reporting/class.llms.admin.reporting.php:251, -#: includes/admin/reporting/tables/llms.table.quizzes.php:189, -#: templates/admin/reporting/tabs/quizzes/quiz.php:13 -msgid "Quizzes" -msgstr "" - -#: includes/class.llms.post-types.php:348, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:159, -#: includes/admin/reporting/tables/llms.table.student.course.php:188, -#: includes/admin/views/builder/editor.php:27 -msgid "Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:349 -msgid "Add Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:350 -msgid "Add New Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:352 -msgid "Edit Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:354, -#: includes/class.llms.post-types.php:355 -msgid "View Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:356 -msgid "Search Quiz" -msgstr "" - -#: includes/class.llms.post-types.php:357 -msgid "No Quizzes found" -msgstr "" - -#: includes/class.llms.post-types.php:358 -msgid "No Quizzes found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:359 -msgid "Parent Quizzes" -msgstr "" - -#: includes/class.llms.post-types.php:360 -msgctxt "Admin menu name" -msgid "Quizzes" -msgstr "" - -#: includes/class.llms.post-types.php:362 -msgid "This is where you can view all of the quizzes." -msgstr "" - -#: includes/class.llms.post-types.php:372 -msgctxt "quiz url slug" -msgid "quiz" -msgstr "" - -#: includes/class.llms.post-types.php:384, -#: includes/admin/class.llms.admin.import.php:94 -msgid "Questions" -msgstr "" - -#: includes/class.llms.post-types.php:385, -#: includes/class.llms.question.types.php:52, -#: includes/admin/reporting/tables/llms.table.questions.php:91 -msgid "Question" -msgstr "" - -#: includes/class.llms.post-types.php:386, -#: includes/admin/views/builder/quiz.php:87 -msgid "Add Question" -msgstr "" - -#: includes/class.llms.post-types.php:387 -msgid "Add New Question" -msgstr "" - -#: includes/class.llms.post-types.php:389 -msgid "Edit Question" -msgstr "" - -#: includes/class.llms.post-types.php:390 -msgid "New Question" -msgstr "" - -#: includes/class.llms.post-types.php:391, -#: includes/class.llms.post-types.php:392 -msgid "View Question" -msgstr "" - -#: includes/class.llms.post-types.php:393 -msgid "Search Questions" -msgstr "" - -#: includes/class.llms.post-types.php:394 -msgid "No Questions found" -msgstr "" - -#: includes/class.llms.post-types.php:395 -msgid "No Questions found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:396 -msgid "Parent Questions" -msgstr "" - -#: includes/class.llms.post-types.php:397 -msgctxt "Admin menu name" -msgid "Quiz Questions" -msgstr "" - -#: includes/class.llms.post-types.php:399 -msgid "This is where you can view all of the Quiz Questions." -msgstr "" - -#: includes/class.llms.post-types.php:409 -msgctxt "quiz question url slug" -msgid "llms_question" -msgstr "" - -#: includes/class.llms.post-types.php:422, -#: includes/class.llms.post-types.php:423, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:165, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:63 -msgid "Membership" -msgstr "" - -#: includes/class.llms.post-types.php:424 -msgctxt "Admin menu name" -msgid "Memberships" -msgstr "" - -#: includes/class.llms.post-types.php:425 -msgid "Add Membership" -msgstr "" - -#: includes/class.llms.post-types.php:426 -msgid "Add New Membership" -msgstr "" - -#: includes/class.llms.post-types.php:428 -msgid "Edit Membership" -msgstr "" - -#: includes/class.llms.post-types.php:429 -msgid "New Membership" -msgstr "" - -#: includes/class.llms.post-types.php:430, -#: includes/class.llms.post-types.php:431 -msgid "View Membership" -msgstr "" - -#: includes/class.llms.post-types.php:432 -msgid "Search Memberships" -msgstr "" - -#: includes/class.llms.post-types.php:433 -msgid "No Memberships found" -msgstr "" - -#: includes/class.llms.post-types.php:434 -msgid "No Memberships found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:435 -msgid "Parent Membership" -msgstr "" - -#: includes/class.llms.post-types.php:437 -msgid "This is where you can add new Membership levels." -msgstr "" - -#: includes/class.llms.post-types.php:448 -msgctxt "slug" -msgid "membership" -msgstr "" - -#: includes/class.llms.post-types.php:466, -#: includes/admin/settings/class.llms.settings.engagements.php:20 -msgid "Engagements" -msgstr "" - -#: includes/class.llms.post-types.php:467 -msgid "Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:468 -msgid "Add Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:469 -msgid "Add New Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:471 -msgid "Edit Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:472 -msgid "New Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:473, -#: includes/class.llms.post-types.php:474 -msgid "View Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:475 -msgid "Search Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:476 -msgid "No Engagement found" -msgstr "" - -#: includes/class.llms.post-types.php:477 -msgid "No Engagement found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:478 -msgid "Parent Engagement" -msgstr "" - -#: includes/class.llms.post-types.php:479 -msgctxt "Admin menu name" -msgid "Engagements" -msgstr "" - -#: includes/class.llms.post-types.php:481 -msgid "This is where engagements are stored." -msgstr "" - -#: includes/class.llms.post-types.php:507, -#: includes/privacy/class-llms-privacy-exporters.php:477 -msgid "Orders" -msgstr "" - -#: includes/class.llms.post-types.php:508, -#: templates/myaccount/my-orders.php:19, templates/myaccount/my-orders.php:29, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:41 -msgid "Order" -msgstr "" - -#: includes/class.llms.post-types.php:509 -msgid "Add Order" -msgstr "" - -#: includes/class.llms.post-types.php:510 -msgid "Add New Order" -msgstr "" - -#: includes/class.llms.post-types.php:512 -msgid "Edit Order" -msgstr "" - -#: includes/class.llms.post-types.php:513 -msgid "New Order" -msgstr "" - -#: includes/class.llms.post-types.php:514, -#: includes/class.llms.post-types.php:515 -msgid "View Order" -msgstr "" - -#: includes/class.llms.post-types.php:516 -msgid "Search Orders" -msgstr "" - -#: includes/class.llms.post-types.php:517 -msgid "No Orders found" -msgstr "" - -#: includes/class.llms.post-types.php:518 -msgid "No Orders found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:519 -msgid "Parent Orders" -msgstr "" - -#: includes/class.llms.post-types.php:520, -#: includes/class.llms.post-types.php:563 -msgctxt "Admin menu name" -msgid "Orders" -msgstr "" - -#: includes/class.llms.post-types.php:522 -msgid "This is where orders are managed" -msgstr "" - -#: includes/class.llms.post-types.php:550, -#: includes/privacy/class-llms-privacy-exporters.php:248, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:22 -msgid "Transactions" -msgstr "" - -#: includes/class.llms.post-types.php:551, -#: templates/myaccount/view-order-transactions.php:17 -msgid "Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:552 -msgid "Add Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:553 -msgid "Add New Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:555 -msgid "Edit Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:556 -msgid "New Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:557, -#: includes/class.llms.post-types.php:558 -msgid "View Transaction" -msgstr "" - -#: includes/class.llms.post-types.php:559 -msgid "Search Transactions" -msgstr "" - -#: includes/class.llms.post-types.php:560 -msgid "No Transactions found" -msgstr "" - -#: includes/class.llms.post-types.php:561 -msgid "No Transactions found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:562 -msgid "Parent Transactions" -msgstr "" - -#: includes/class.llms.post-types.php:565 -msgid "This is where single and recurring order transactions are stored" -msgstr "" - -#: includes/class.llms.post-types.php:592, -#: includes/integrations/class.llms.integration.buddypress.php:83, -#: includes/privacy/class-llms-privacy-exporters.php:31, -#: includes/admin/reporting/tables/llms.table.students.php:445, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:85 -msgid "Achievements" -msgstr "" - -#: includes/class.llms.post-types.php:593 -msgid "Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:594 -msgid "Add Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:595 -msgid "Add New Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:597 -msgid "Edit Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:598 -msgid "New Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:599, -#: includes/class.llms.post-types.php:600 -msgid "View Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:601 -msgid "Search Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:602 -msgid "No Achievement found" -msgstr "" - -#: includes/class.llms.post-types.php:603 -msgid "No Achievement found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:604 -msgid "Parent Achievement" -msgstr "" - -#: includes/class.llms.post-types.php:605 -msgctxt "Admin menu name" -msgid "Achievements" -msgstr "" - -#: includes/class.llms.post-types.php:607 -msgid "This is where achievements are stored." -msgstr "" - -#: includes/class.llms.post-types.php:631, -#: includes/integrations/class.llms.integration.buddypress.php:92, -#: includes/privacy/class-llms-privacy-exporters.php:68, -#: includes/admin/reporting/tables/llms.table.students.php:441, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:86 -msgid "Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:632 -msgid "Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:633 -msgid "Add Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:634 -msgid "Add New Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:636 -msgid "Edit Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:637 -msgid "New Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:638, -#: includes/class.llms.post-types.php:639 -msgid "View Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:640 -msgid "Search Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:641 -msgid "No Certificates found" -msgstr "" - -#: includes/class.llms.post-types.php:642 -msgid "No Certificates found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:643 -msgid "Parent Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:644 -msgctxt "Admin menu name" -msgid "Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:646, -#: includes/class.llms.post-types.php:688 -msgid "This is where you can view all of the certificates." -msgstr "" - -#: includes/class.llms.post-types.php:655 -msgctxt "slug" -msgid "certificate" -msgstr "" - -#: includes/class.llms.post-types.php:673, -#: includes/class.llms.student.dashboard.php:153, -#: includes/functions/llms.functions.templates.dashboard.php:221 -msgid "My Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:674 -msgid "My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:675 -msgid "Add My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:676 -msgid "Add New My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:678 -msgid "Edit My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:679 -msgid "New My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:680, -#: includes/class.llms.post-types.php:681 -msgid "View My Certificate" -msgstr "" - -#: includes/class.llms.post-types.php:682 -msgid "Search My Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:683 -msgid "No My Certificates found" -msgstr "" - -#: includes/class.llms.post-types.php:684 -msgid "No My Certificates found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:685 -msgid "Parent My Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:686 -msgctxt "Admin menu name" -msgid "My Certificates" -msgstr "" - -#: includes/class.llms.post-types.php:697 -msgctxt "slug" -msgid "my_certificate" -msgstr "" - -#: includes/class.llms.post-types.php:715 -msgid "Emails" -msgstr "" - -#: includes/class.llms.post-types.php:716, -#: includes/abstracts/llms.abstract.notification.controller.php:420, -#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:96, -#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:96, -#: includes/notifications/controllers/class.llms.notification.controller.purchase.receipt.php:98, -#: includes/notifications/controllers/class.llms.notification.controller.student.welcome.php:83, -#: includes/notifications/controllers/class.llms.notification.controller.subscription.cancelled.php:125, -#: includes/admin/reporting/tables/llms.table.course.students.php:387, -#: includes/admin/reporting/tables/llms.table.students.php:400 -msgid "Email" -msgstr "" - -#: includes/class.llms.post-types.php:717 -msgid "Add Email" -msgstr "" - -#: includes/class.llms.post-types.php:718 -msgid "Add New Email" -msgstr "" - -#: includes/class.llms.post-types.php:720 -msgid "Edit Email" -msgstr "" - -#: includes/class.llms.post-types.php:721 -msgid "New Email" -msgstr "" - -#: includes/class.llms.post-types.php:722, -#: includes/class.llms.post-types.php:723 -msgid "View Email" -msgstr "" - -#: includes/class.llms.post-types.php:724 -msgid "Search Emails" -msgstr "" - -#: includes/class.llms.post-types.php:725 -msgid "No Emails found" -msgstr "" - -#: includes/class.llms.post-types.php:726 -msgid "No Emails found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:727 -msgid "Parent Emails" -msgstr "" - -#: includes/class.llms.post-types.php:728 -msgctxt "Admin menu name" -msgid "Emails" -msgstr "" - -#: includes/class.llms.post-types.php:730 -msgid "This is where emails are stored." -msgstr "" - -#: includes/class.llms.post-types.php:754 -msgid "Coupons" -msgstr "" - -#: includes/class.llms.post-types.php:755, -#: includes/admin/class.llms.admin.setup.wizard.php:190 -msgid "Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:756 -msgid "Add Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:757 -msgid "Add New Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:759 -msgid "Edit Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:760 -msgid "New Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:761, -#: includes/class.llms.post-types.php:762 -msgid "View Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:763 -msgid "Search Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:764 -msgid "No Coupon found" -msgstr "" - -#: includes/class.llms.post-types.php:765 -msgid "No Coupon found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:766 -msgid "Parent Coupon" -msgstr "" - -#: includes/class.llms.post-types.php:767 -msgctxt "Admin menu name" -msgid "Coupons" -msgstr "" - -#: includes/class.llms.post-types.php:769 -msgid "This is where coupons are stored." -msgstr "" - -#: includes/class.llms.post-types.php:793 -msgid "Vouchers" -msgstr "" - -#: includes/class.llms.post-types.php:794, -#: includes/admin/settings/class.llms.settings.accounts.php:419 -msgid "Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:795 -msgid "Add Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:796 -msgid "Add New Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:798 -msgid "Edit Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:799 -msgid "New Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:800, -#: includes/class.llms.post-types.php:801 -msgid "View Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:802 -msgid "Search Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:803 -msgid "No Voucher found" -msgstr "" - -#: includes/class.llms.post-types.php:804 -msgid "No Voucher found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:805 -msgid "Parent Voucher" -msgstr "" - -#: includes/class.llms.post-types.php:806 -msgctxt "Admin menu name" -msgid "Vouchers" -msgstr "" - -#: includes/class.llms.post-types.php:808 -msgid "This is where voucher are stored." -msgstr "" - -#: includes/class.llms.post-types.php:832, -#: includes/admin/class.llms.admin.reviews.php:152 -msgid "Reviews" -msgstr "" - -#: includes/class.llms.post-types.php:833 -msgid "Review" -msgstr "" - -#: includes/class.llms.post-types.php:834 -msgctxt "Admin menu name" -msgid "Reviews" -msgstr "" - -#: includes/class.llms.post-types.php:835 -msgid "Add Review" -msgstr "" - -#: includes/class.llms.post-types.php:836 -msgid "Add New Review" -msgstr "" - -#: includes/class.llms.post-types.php:838 -msgid "Edit Review" -msgstr "" - -#: includes/class.llms.post-types.php:839 -msgid "New Review" -msgstr "" - -#: includes/class.llms.post-types.php:840, -#: includes/class.llms.post-types.php:841 -msgid "View Review" -msgstr "" - -#: includes/class.llms.post-types.php:842 -msgid "Search Reviews" -msgstr "" - -#: includes/class.llms.post-types.php:843 -msgid "No Reviews found" -msgstr "" - -#: includes/class.llms.post-types.php:844 -msgid "No Reviews found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:845 -msgid "Parent Review" -msgstr "" - -#: includes/class.llms.post-types.php:847 -msgid "This is where you can add new reviews." -msgstr "" - -#: includes/class.llms.post-types.php:871 -msgid "Access Plans" -msgstr "" - -#: includes/class.llms.post-types.php:872, -#: templates/checkout/form-summary.php:11, -#: templates/myaccount/view-order.php:42 -msgid "Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:873, -#: templates/admin/post-types/product.php:24 -msgid "Add Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:874 -msgid "Add New Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:876 -msgid "Edit Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:877, -#: templates/admin/post-types/product-access-plan.php:45, -#: templates/admin/post-types/product-access-plan.php:45 -msgid "New Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:878, -#: includes/class.llms.post-types.php:879 -msgid "View Access Plan" -msgstr "" - -#: includes/class.llms.post-types.php:880 -msgid "Search Access Plans" -msgstr "" - -#: includes/class.llms.post-types.php:881 -msgid "No Access Plans found" -msgstr "" - -#: includes/class.llms.post-types.php:882 -msgid "No Access Plans found in trash" -msgstr "" - -#: includes/class.llms.post-types.php:883 -msgid "Parent Access Plans" -msgstr "" - -#: includes/class.llms.post-types.php:884 -msgctxt "Admin menu name" -msgid "Access Plans" -msgstr "" - -#: includes/class.llms.post-types.php:886 -msgid "This is where access plans are stored." -msgstr "" - -#: includes/class.llms.post-types.php:926 -msgctxt "Transaction status" -msgid "Failed" -msgstr "" - -#: includes/class.llms.post-types.php:934 -msgctxt "Transaction status" -msgid "Pending" -msgstr "" - -#: includes/class.llms.post-types.php:939 -msgid "Pending <span class=\"count\">(%s)</span>" -msgid_plural "Pending <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:942 -msgctxt "Transaction status" -msgid "Refunded" -msgstr "" - -#: includes/class.llms.post-types.php:950 -msgctxt "Transaction status" -msgid "Succeeded" -msgstr "" - -#: includes/class.llms.post-types.php:955 -msgid "Succeeded <span class=\"count\">(%s)</span>" -msgid_plural "Succeeded <span class=\"count\">(%s)</span>" -msgstr[0] "" -msgstr[1] "" - -#: includes/class.llms.post-types.php:1000, -#: includes/class.llms.post-types.php:1002 -msgid "Course Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1003 -msgid "Course Category" -msgstr "" - -#: includes/class.llms.post-types.php:1004, -#: includes/class.llms.post-types.php:1116 -msgctxt "Admin menu name" -msgid "Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1005 -msgid "Search Course Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1006 -msgid "All Course Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1007 -msgid "Parent Course Category" -msgstr "" - -#: includes/class.llms.post-types.php:1008 -msgid "Parent Course Category:" -msgstr "" - -#: includes/class.llms.post-types.php:1009 -msgid "Edit Course Category" -msgstr "" - -#: includes/class.llms.post-types.php:1010 -msgid "Update Course Category" -msgstr "" - -#: includes/class.llms.post-types.php:1011 -msgid "Add New Course Category" -msgstr "" - -#: includes/class.llms.post-types.php:1012 -msgid "New Course Category Name" -msgstr "" - -#: includes/class.llms.post-types.php:1020 -msgctxt "slug" -msgid "course-category" -msgstr "" - -#: includes/class.llms.post-types.php:1028, -#: includes/class.llms.post-types.php:1030 -msgid "Course Difficulties" -msgstr "" - -#: includes/class.llms.post-types.php:1031 -msgid "Course Difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1032 -msgctxt "Admin menu name" -msgid "Difficulties" -msgstr "" - -#: includes/class.llms.post-types.php:1033 -msgid "Search Course Difficulties" -msgstr "" - -#: includes/class.llms.post-types.php:1034 -msgid "All Course Difficulties" -msgstr "" - -#: includes/class.llms.post-types.php:1035 -msgid "Parent Course Difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1036 -msgid "Parent Course Difficulty:" -msgstr "" - -#: includes/class.llms.post-types.php:1037 -msgid "Edit Course Difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1038 -msgid "Update Course Difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1039 -msgid "Add New Course Difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1040 -msgid "New Course Difficulty Name" -msgstr "" - -#: includes/class.llms.post-types.php:1048 -msgctxt "slug" -msgid "course-difficulty" -msgstr "" - -#: includes/class.llms.post-types.php:1056, -#: includes/class.llms.post-types.php:1058 -msgid "Course Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1059 -msgid "Course Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1060, -#: includes/class.llms.post-types.php:1145 -msgctxt "Admin menu name" -msgid "Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1061 -msgid "Search Course Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1062 -msgid "All Course Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1063 -msgid "Parent Course Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1064 -msgid "Parent Course Tag:" -msgstr "" - -#: includes/class.llms.post-types.php:1065 -msgid "Edit Course Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1066 -msgid "Update Course Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1067 -msgid "Add New Course Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1068 -msgid "New Course Tag Name" -msgstr "" - -#: includes/class.llms.post-types.php:1076 -msgctxt "slug" -msgid "course-tag" -msgstr "" - -#: includes/class.llms.post-types.php:1083, -#: includes/class.llms.post-types.php:1086 -msgid "Course Track" -msgstr "" - -#: includes/class.llms.post-types.php:1085 -msgid "Course Tracks" -msgstr "" - -#: includes/class.llms.post-types.php:1087 -msgctxt "Admin menu name" -msgid "Tracks" -msgstr "" - -#: includes/class.llms.post-types.php:1088 -msgid "Search Course Tracks" -msgstr "" - -#: includes/class.llms.post-types.php:1089 -msgid "All Course Tracks" -msgstr "" - -#: includes/class.llms.post-types.php:1090 -msgid "Parent Course Track" -msgstr "" - -#: includes/class.llms.post-types.php:1091 -msgid "Parent Course Track:" -msgstr "" - -#: includes/class.llms.post-types.php:1092 -msgid "Edit Course Track" -msgstr "" - -#: includes/class.llms.post-types.php:1093 -msgid "Update Course Track" -msgstr "" - -#: includes/class.llms.post-types.php:1094 -msgid "Add New Course Track" -msgstr "" - -#: includes/class.llms.post-types.php:1095 -msgid "New Course Track Name" -msgstr "" - -#: includes/class.llms.post-types.php:1103 -msgctxt "slug" -msgid "course-track" -msgstr "" - -#: includes/class.llms.post-types.php:1112, -#: includes/class.llms.post-types.php:1114 -msgid "Membership Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1115 -msgid "Membership Category" -msgstr "" - -#: includes/class.llms.post-types.php:1117 -msgid "Search Membership Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1118 -msgid "All Membership Categories" -msgstr "" - -#: includes/class.llms.post-types.php:1119 -msgid "Parent Membership Category" -msgstr "" - -#: includes/class.llms.post-types.php:1120 -msgid "Parent Membership Category:" -msgstr "" - -#: includes/class.llms.post-types.php:1121 -msgid "Edit Membership Category" -msgstr "" - -#: includes/class.llms.post-types.php:1122 -msgid "Update Membership Category" -msgstr "" - -#: includes/class.llms.post-types.php:1123 -msgid "Add New Membership Category" -msgstr "" - -#: includes/class.llms.post-types.php:1124 -msgid "New Membership Category Name" -msgstr "" - -#: includes/class.llms.post-types.php:1132 -msgctxt "slug" -msgid "membership-category" -msgstr "" - -#: includes/class.llms.post-types.php:1141, -#: includes/class.llms.post-types.php:1143 -msgid "Membership Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1144 -msgid "Membership Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1146 -msgid "Search Membership Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1147 -msgid "All Membership Tags" -msgstr "" - -#: includes/class.llms.post-types.php:1148 -msgid "Parent Membership Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1149 -msgid "Parent Membership Tag:" -msgstr "" - -#: includes/class.llms.post-types.php:1150 -msgid "Edit Membership Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1151 -msgid "Update Membership Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1152 -msgid "Add New Membership Tag" -msgstr "" - -#: includes/class.llms.post-types.php:1153 -msgid "New Membership Tag Name" -msgstr "" - -#: includes/class.llms.post-types.php:1161 -msgctxt "slug" -msgid "membership-tag" -msgstr "" - -#: includes/class.llms.question.types.php:47 -msgid "Other" -msgstr "" - -#: includes/class.llms.question.types.php:53 -msgid "Enter your question..." -msgstr "" - -#: includes/class.llms.question.types.php:77, -#: includes/class.llms.question.types.php:90, -#: includes/class.llms.question.types.php:116 -msgid "Basic Questions" -msgstr "" - -#: includes/class.llms.question.types.php:81 -msgid "Multiple Choice" -msgstr "" - -#: includes/class.llms.question.types.php:94 -msgid "Picture Choice" -msgstr "" - -#: includes/class.llms.question.types.php:105 -msgid "True" -msgstr "" - -#: includes/class.llms.question.types.php:110 -msgid "False" -msgstr "" - -#: includes/class.llms.question.types.php:120 -msgid "True or False" -msgstr "" - -#: includes/class.llms.question.types.php:129 -msgid "Content" -msgstr "" - -#: includes/class.llms.question.types.php:130 -msgid "Enter your content title..." -msgstr "" - -#: includes/class.llms.question.types.php:153, -#: includes/class.llms.question.types.php:165, -#: includes/class.llms.question.types.php:177, -#: includes/class.llms.question.types.php:189, -#: includes/class.llms.question.types.php:201, -#: includes/class.llms.question.types.php:213, -#: includes/class.llms.question.types.php:225, -#: includes/class.llms.question.types.php:237 -msgid "Advanced Questions" -msgstr "" - -#: includes/class.llms.question.types.php:157 -msgid "Fill in the Blank" -msgstr "" - -#: includes/class.llms.question.types.php:169 -msgid "Reorder Items" -msgstr "" - -#: includes/class.llms.question.types.php:181 -msgid "Reorder Pictures" -msgstr "" - -#: includes/class.llms.question.types.php:193 -msgid "Short Answer" -msgstr "" - -#: includes/class.llms.question.types.php:205 -msgid "Long Answer" -msgstr "" - -#: includes/class.llms.question.types.php:217 -msgid "File Upload" -msgstr "" - -#: includes/class.llms.question.types.php:229, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:33 -msgid "Code" -msgstr "" - -#: includes/class.llms.question.types.php:241 -msgid "Scale" -msgstr "" - -#: includes/class.llms.quiz.legacy.php:411, -#: includes/models/model.llms.student.quizzes.php:129 -msgctxt "quiz attempts remaining" -msgid "Unlimited" -msgstr "" - -#: includes/class.llms.review.php:42 -msgid "What Others Have Said" -msgstr "" - -#: includes/class.llms.review.php:70 -msgid "By: %s" -msgstr "" - -#: includes/class.llms.review.php:106, includes/class.llms.review.php:125 -msgid "Thank you for your review!" -msgstr "" - -#: includes/class.llms.review.php:112 -msgid "Write a Review" -msgstr "" - -#: includes/class.llms.review.php:114, -#: includes/admin/class.llms.admin.reviews.php:48 -msgid "Review Title" -msgstr "" - -#: includes/class.llms.review.php:115 -msgid "Review Title is required." -msgstr "" - -#: includes/class.llms.review.php:116 -msgid "Review Text" -msgstr "" - -#: includes/class.llms.review.php:117 -msgid "Review Text is required." -msgstr "" - -#: includes/class.llms.review.php:121 -msgid "Leave Review" -msgstr "" - -#: includes/class.llms.roles.php:280, -#: tests/unit-tests/class.llms.test.roles.php:22 -msgid "LMS Manager" -msgstr "" - -#: includes/class.llms.roles.php:281, -#: tests/unit-tests/class.llms.test.roles.php:20, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:65 -msgid "Instructor" -msgstr "" - -#: includes/class.llms.roles.php:282, -#: tests/unit-tests/class.llms.test.roles.php:21 -msgid "Instructor's Assistant" -msgstr "" - -#: includes/class.llms.roles.php:283, includes/class.llms.view.manager.php:188, -#: includes/abstracts/llms.abstract.notification.controller.php:256, -#: includes/functions/llms.functions.updates.php:717, -#: tests/unit-tests/class.llms.test.roles.php:23, -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:257 -msgid "Student" -msgstr "" - -#: includes/class.llms.roles.php:306 -msgid "Administrator" -msgstr "" - -#: includes/class.llms.sidebars.php:171 -msgid "Widgets in this area will be shown on LifterLMS courses." -msgstr "" - -#: includes/class.llms.sidebars.php:172 -msgid "Course Sidebar" -msgstr "" - -#: includes/class.llms.sidebars.php:175 -msgid "Widgets in this area will be shown on LifterLMS lessons." -msgstr "" - -#: includes/class.llms.sidebars.php:177 -msgid "Lesson Sidebar" -msgstr "" - -#: includes/class.llms.student.dashboard.php:128 -msgid "Dashboard" -msgstr "" - -#: includes/class.llms.student.dashboard.php:141, -#: includes/functions/llms.functions.templates.dashboard.php:293 -msgid "My Memberships" -msgstr "" - -#: includes/class.llms.student.dashboard.php:147, -#: includes/functions/llms.functions.templates.dashboard.php:185 -msgid "My Achievements" -msgstr "" - -#: includes/class.llms.student.dashboard.php:159, -#: includes/admin/settings/class.llms.settings.accounts.php:145, -#: includes/admin/settings/class.llms.settings.notifications.php:17 -msgid "Notifications" -msgstr "" - -#: includes/class.llms.student.dashboard.php:165, -#: includes/admin/settings/class.llms.settings.accounts.php:154 -msgid "Edit Account" -msgstr "" - -#: includes/class.llms.student.dashboard.php:171 -msgid "Redeem a Voucher" -msgstr "" - -#: includes/class.llms.student.dashboard.php:177 -msgid "Order History" -msgstr "" - -#: includes/class.llms.student.dashboard.php:293 -msgid "View Notifications" -msgstr "" - -#: includes/class.llms.student.dashboard.php:297 -msgid "Manage Preferences" -msgstr "" - -#: includes/class.llms.template.loader.php:335 -msgid "You must be enrolled in the course to access this quiz." -msgstr "" - -#: includes/class.llms.view.manager.php:90 -msgid "Viewing as %s" -msgstr "" - -#: includes/class.llms.view.manager.php:108 -msgid "View as %s" -msgstr "" - -#: includes/class.llms.view.manager.php:186 -msgid "Myself" -msgstr "" - -#: includes/class.llms.view.manager.php:187 -msgid "Visitor" -msgstr "" - -#: includes/class.llms.voucher.php:260 -msgid "Voucher code \"%s\" could not be found." -msgstr "" - -#: includes/class.llms.voucher.php:264 -msgid "" -"Voucher code \"%s\" has already been redeemed the maximum number of times." -msgstr "" - -#: includes/class.llms.voucher.php:268 -msgid "Voucher code \"%s\" is no longer valid." -msgstr "" - -#: includes/class.llms.voucher.php:296 -msgid "You have already redeemed this voucher." -msgstr "" - -#: includes/llms.functions.core.php:126 -msgid "" -"%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead." -msgstr "" - -#: includes/llms.functions.core.php:128 -msgid "%1$s is <strong>deprecated</strong> since version %2$s!" -msgstr "" - -#: includes/llms.functions.core.php:186, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:84 -msgid "Visible" -msgstr "" - -#: includes/llms.functions.core.php:187, includes/llms.functions.core.php:404, -#: includes/admin/settings/class.llms.settings.accounts.php:44, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:85 -msgid "Hidden" -msgstr "" - -#: includes/llms.functions.core.php:188 -msgid "Featured" -msgstr "" - -#: includes/llms.functions.core.php:249, -#: templates/admin/post-types/product-access-plan.php:121 -msgid "year" -msgstr "" - -#: includes/llms.functions.core.php:250, -#: templates/admin/post-types/product-access-plan.php:122 -msgid "month" -msgstr "" - -#: includes/llms.functions.core.php:251, -#: templates/admin/post-types/product-access-plan.php:124 -msgid "day" -msgstr "" - -#: includes/llms.functions.core.php:257 -msgid "years" -msgstr "" - -#: includes/llms.functions.core.php:258 -msgid "months" -msgstr "" - -#: includes/llms.functions.core.php:259 -msgid "days" -msgstr "" - -#: includes/llms.functions.core.php:337 -msgid "Student creates a new account" -msgstr "" - -#: includes/llms.functions.core.php:338 -msgid "Student Purchases an Access Plan" -msgstr "" - -#: includes/llms.functions.core.php:339 -msgid "Student enrolls in a course" -msgstr "" - -#: includes/llms.functions.core.php:340 -msgid "Student purchases a course" -msgstr "" - -#: includes/llms.functions.core.php:341 -msgid "Student completes a course" -msgstr "" - -#: includes/llms.functions.core.php:343 -msgid "Student completes a lesson" -msgstr "" - -#: includes/llms.functions.core.php:344 -msgid "Student completes a quiz" -msgstr "" - -#: includes/llms.functions.core.php:345 -msgid "Student passes a quiz" -msgstr "" - -#: includes/llms.functions.core.php:346 -msgid "Student fails a quiz" -msgstr "" - -#: includes/llms.functions.core.php:347 -msgid "Student completes a section" -msgstr "" - -#: includes/llms.functions.core.php:348 -msgid "Student completes a course track" -msgstr "" - -#: includes/llms.functions.core.php:349 -msgid "Student enrolls in a membership" -msgstr "" - -#: includes/llms.functions.core.php:350 -msgid "Student purchases a membership" -msgstr "" - -#: includes/llms.functions.core.php:362 -msgid "Award an Achievement" -msgstr "" - -#: includes/llms.functions.core.php:363 -msgid "Award a Certificate" -msgstr "" - -#: includes/llms.functions.core.php:401 -msgid "Catalog & Search" -msgstr "" - -#: includes/llms.functions.core.php:402 -msgid "Catalog only" -msgstr "" - -#: includes/llms.functions.core.php:403 -msgid "Search only" -msgstr "" - -#: includes/llms.functions.core.php:624 -msgid "Cancelled" -msgstr "" - -#: includes/llms.functions.core.php:625, -#: includes/admin/reporting/tables/llms.table.student.memberships.php:113 -msgid "Enrolled" -msgstr "" - -#: includes/llms.functions.core.php:626 -msgid "Expired" -msgstr "" - -#: includes/llms.template.functions.php:768 -msgid "Search Results: “%s”" -msgstr "" - -#: includes/llms.template.functions.php:771 -msgid " – Page %s" -msgstr "" - -#: includes/llms.template.functions.php:823 -msgid "%s" -msgstr "" - -#: includes/llms.template.functions.php:830, -#: includes/llms.template.functions.php:902 -msgid "Continue" -msgstr "" - -#: includes/llms.template.functions.php:887, -#: includes/notifications/controllers/class.llms.notification.controller.course.complete.php:85 -msgid "Course Complete" -msgstr "" - -#: includes/llms.template.functions.php:898 -msgid "Get Started" -msgstr "" - -#: templates/content-certificate.php:14 -msgid "Certificate not found." -msgstr "" - -#: templates/content-certificate.php:40 -msgid "Print" -msgstr "" - -#: templates/content-certificate.php:46, -#: templates/myaccount/form-edit-account.php:44 -msgid "Save" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:381 -msgid "Any" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:383, -#: includes/abstracts/abstract.llms.admin.table.php:385 -msgid "Any %s" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:578 -msgid "Search" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:645, -#: includes/admin/post-types/class.llms.post.tables.php:53, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:109 -msgid "Export" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:656 -msgctxt "pagination" -msgid "%d of %d" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:660 -msgid "First" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:662, -#: templates/myaccount/my-notifications.php:37, -#: templates/myaccount/my-orders.php:61, -#: templates/myaccount/view-order-transactions.php:49 -msgid "Back" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:665, -#: includes/shortcodes/class.llms.shortcodes.php:290, -#: templates/loop/pagination.php:24, -#: templates/myaccount/my-notifications.php:41, -#: templates/myaccount/my-orders.php:67, -#: templates/myaccount/view-order-transactions.php:52 -msgid "Next" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:667 -msgid "Last" -msgstr "" - -#: includes/abstracts/abstract.llms.admin.table.php:881 -msgid "No results were found." -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:220 -msgid "Customer ID" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:225 -msgid "Source ID" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:230 -msgid "Subscription ID" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:259 -msgctxt "Payment gateway title" -msgid "Enable %s" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:260 -msgid "Checking this box will allow users to use this payment gateway." -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:262, -#: includes/abstracts/llms.abstract.integration.php:115, -#: includes/admin/settings/class.llms.settings.accounts.php:245, -#: includes/admin/settings/class.llms.settings.accounts.php:377 -msgid "Enable / Disable" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:268 -msgid "The title the user sees during checkout." -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:270, -#: includes/abstracts/llms.abstract.notification.view.php:318, -#: includes/privacy/class-llms-privacy-exporters.php:97, -#: includes/privacy/class-llms-privacy-exporters.php:141, -#: includes/privacy/class-llms-privacy-exporters.php:270, -#: includes/notifications/views/class.llms.notification.view.enrollment.php:73, -#: includes/admin/reporting/tables/llms.table.courses.php:276, -#: includes/admin/reporting/tables/llms.table.quizzes.php:300, -#: includes/admin/views/builder/lesson-settings.php:13, -#: includes/admin/views/builder/quiz.php:35 -msgid "Title" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:276 -msgid "The description the user sees during checkout." -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:284 -msgid "" -"This determines the order gateways are displayed on the checkout page. " -"Lowest number will display first." -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:286 -msgid "Display Order" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:294 -msgctxt "Payment gateway test mode title" -msgid "Enable %s" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:305 -msgid "Enable debug logging" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:306 -msgid "When enabled, debugging information will be logged to \"%s\"" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:307 -msgid "Debug Log" -msgstr "" - -#: includes/abstracts/abstract.llms.payment.gateway.php:558 -msgid "" -"The selected payment Gateway \"%s\" does not support payment method " -"switching." -msgstr "" - -#: includes/abstracts/abstract.llms.post.model.php:332 -msgid "An unknown error occurred during post cloning. Please try again." -msgstr "" - -#: includes/abstracts/abstract.llms.update.php:260 -msgid "LifterLMS Database Upgrade %s Progress Report" -msgstr "" - -#: includes/abstracts/abstract.llms.update.php:262 -msgid "" -"This completion percentage is an estimate, please be patient and %sclick here" -"%s for more information." -msgstr "" - -#: includes/abstracts/llms.abstract.integration.php:111 -msgid "Check to enable this integration." -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:253, -#: includes/models/model.llms.post.instructors.php:55, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:43 -msgid "Author" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:259 -msgid "Lesson Author" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:262 -msgid "Course Author" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:265 -msgid "" -"Enter additional email addresses which will receive this notification. " -"Separate multiple addresses with commas." -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:266 -msgid "Additional Recipients" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.controller.php:419, -#: includes/notifications/controllers/class.llms.notification.controller.achievement.earned.php:118, -#: includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php:118, -#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:95, -#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:95 -msgid "Basic" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:253 -msgctxt "relative date display" -msgid "About %s ago" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:308 -msgid "Subject" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:318 -msgid "Heading" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:330 -msgid "Body" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:340 -msgid "Icon" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:346 -msgid "" -"When checked the icon will not be displayed when showing this notification." -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:348 -msgid "Disable Icon" -msgstr "" - -#: includes/abstracts/llms.abstract.notification.view.php:468 -msgid "Divider Line" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:87 -msgid "Advanced" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:91 -msgid "Affiliates" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:95 -msgid "All" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:99 -msgid "Bundles" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:103, -#: includes/admin/settings/class.llms.settings.checkout.php:310 -msgid "Payment Gateways" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:107 -msgid "E-Mail & Marketing" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:111 -msgid "Themes & Design" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:115, -#: includes/admin/class.llms.admin.page.status.php:128 -msgid "Tools & Utilities" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:119 -msgid "Resources" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:123 -msgid "Services" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:140, -#: includes/admin/class.llms.admin.addons.php:225 -msgid "There was an error retrieving add-ons. Please try again." -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:146 -msgid "LifterLMS Add-Ons, Services, and Resources" -msgstr "" - -#: includes/admin/class.llms.admin.addons.php:173 -msgid "Created by:" -msgstr "" - -#: includes/admin/class.llms.admin.analytics.php:79, -#: includes/admin/class.llms.admin.settings.php:70 -msgid "Whoa! something went wrong there!. Please refresh the page and retry." -msgstr "" - -#: includes/admin/class.llms.admin.analytics.php:86 -msgid "Your analytics have been saved." -msgstr "" - -#: includes/admin/class.llms.admin.analytics.php:182, -#: includes/admin/class.llms.admin.analytics.php:251, -#: includes/admin/class.llms.admin.analytics.php:333 -msgid "You must choose a product option." -msgstr "" - -#: includes/admin/class.llms.admin.analytics.php:185, -#: includes/admin/class.llms.admin.analytics.php:254, -#: includes/admin/class.llms.admin.analytics.php:336 -msgid "You must choose a date filter." -msgstr "" - -#: includes/admin/class.llms.admin.analytics.php:188, -#: includes/admin/class.llms.admin.analytics.php:257, -#: includes/admin/class.llms.admin.analytics.php:339 -msgid "You must enter a start and end date." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:84 -msgid "%s Theme Settings" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:176 -msgid "Lesson: %1$s (#%2$d)" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:179 -msgid "Course: %1$s (#%2$d)" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:367 -msgid "Success" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:374 -msgid "Error: Invalid or missing course ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:379 -msgid "Error: You do not have permission to edit this course." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:456 -msgid "Invalid course ID" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:465 -msgid "You cannot edit this course!" -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:547 -msgid "Unable to detach \"%s\". Invalid ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:602 -msgid "Unable to delete \"%s\". Invalid ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:651 -msgid "Error deleting %1$s \"%s\"." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:793 -msgid "Unable to update lesson \"%s\". Invalid lesson ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:882 -msgid "Unable to update question \"%s\". Invalid question ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:909 -msgid "Unable to update choice \"%s\". Invalid choice ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:964 -msgid "Unable to update quiz \"%s\". Invalid quiz ID." -msgstr "" - -#: includes/admin/class.llms.admin.builder.php:1032 -msgid "Unable to update section \"%s\". Invalid section ID." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:60 -msgid "Import Successful" -msgstr "" - -#: includes/admin/class.llms.admin.import.php:70 -msgid "Authors" -msgstr "" - -#: includes/admin/class.llms.admin.import.php:86 -msgid "Plans" -msgstr "" - -#: includes/admin/class.llms.admin.import.php:98, -#: templates/checkout/form-summary.php:23 -msgid "Terms" -msgstr "" - -#: includes/admin/class.llms.admin.import.php:128 -msgid "The uploaded file exceeds the upload_max_filesize directive in php.ini." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:132 -msgid "" -"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in " -"the HTML form." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:136 -msgid "The uploaded file was only partially uploaded." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:140 -msgid "No file was uploaded." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:144 -msgid "Missing a temporary folder." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:148 -msgid "Failed to write file to disk." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:152 -msgid "File upload stopped by extension." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:156 -msgid "Unknown upload error." -msgstr "" - -#: includes/admin/class.llms.admin.import.php:163 -msgid "Only valid JSON files can be imported." -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:114, -#: includes/admin/class.llms.admin.menus.php:139, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:21 -msgid "Course Builder" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:131 -msgid "LifterLMS Settings" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:133 -msgid "LifterLMS Reporting" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:133 -msgid "Reporting" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:135 -msgid "LifterLMS Import" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:135, -#: templates/admin/import/import.php:26 -msgid "Import" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:137 -msgid "LifterLMS Status" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:137, -#: includes/privacy/class-llms-privacy-exporters.php:286, -#: templates/myaccount/view-order.php:37, -#: templates/admin/post-types/order-transactions.php:18, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:356, -#: includes/admin/reporting/tables/llms.table.course.students.php:393, -#: includes/admin/reporting/tables/llms.table.student.courses.php:203, -#: includes/admin/reporting/tables/llms.table.student.memberships.php:110, -#: templates/admin/reporting/tabs/quizzes/attempt.php:76 -msgid "Status" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:139 -msgid "LifterLMS Course Builder" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:158 -msgid "LifterLMS Add-ons" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:158 -msgid "Add-ons" -msgstr "" - -#: includes/admin/class.llms.admin.menus.php:232, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:86, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:67, -#: templates/admin/reporting/tabs/students/courses.php:33 -msgid "You do not have permission to access this content." -msgstr "" - -#: includes/admin/class.llms.admin.notices.core.php:114 -msgid "" -"No LifterLMS Payment Gateways are currently enabled. Students will only be " -"able to enroll in courses or memberships with free access plans." -msgstr "" - -#: includes/admin/class.llms.admin.notices.core.php:115 -msgid "" -"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." -msgstr "" - -#: includes/admin/class.llms.admin.notices.core.php:163 -msgid "" -"<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." -msgstr "" - -#: includes/admin/class.llms.admin.notices.php:237 -msgid "Dismiss" -msgstr "" - -#: includes/admin/class.llms.admin.notices.php:252 -msgid "Remind me later" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:127 -msgid "System Report" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:129 -msgid "Logs" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:237 -msgid "View Log" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:241 -msgid "Viewing: %s" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:244 -msgid "Delete Log" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:252 -msgid "There are currently no logs to view." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:268 -msgid "" -"Allows you to choose to enable or disable automatic recurring payments which " -"may be disabled on a staging site." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:269 -msgid "Automatic Payments" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:270 -msgid "Reset Automatic Payments" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:274 -msgid "" -"Manage User Sessions. LifterLMS creates custom user sessions to manage, " -"payment processing, quizzes and user registration. If you are experiencing " -"issues or incorrect error messages are displaying. Clearing out all of the " -"user session data may help." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:275 -msgid "User Sessions" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:276 -msgid "Clear All Session Data" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:280 -msgid "" -"If you opted into LifterLMS Tracking and no longer wish to participate, you " -"may opt out here." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:281, -#: includes/admin/class.llms.admin.page.status.php:282 -msgid "Reset Tracking Settings" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:286 -msgid "" -"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." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:287 -msgid "Student Progress Cache" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:288 -msgid "Clear cache" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:292 -msgid "" -"If you want to run the LifterLMS Setup Wizard again or skipped it and want " -"to return now, click below." -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:293 -msgid "Setup Wizard" -msgstr "" - -#: includes/admin/class.llms.admin.page.status.php:294 -msgid "Return to Setup Wizard" -msgstr "" - -#: includes/admin/class.llms.admin.post-types.php:80 -msgid "Custom field updated." -msgstr "" - -#: includes/admin/class.llms.admin.post-types.php:81 -msgid "Custom field deleted." -msgstr "" - -#: includes/admin/class.llms.admin.post-types.php:88 -msgid "M j, Y @ G:i" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:49 -msgid "Course Reviewed" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:50 -msgid "Review Author" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:51 -msgid "Review Date" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:106 -msgid "Enable Reviews" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:107 -msgid "Select to enable reviews." -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:116 -msgid "Display Reviews" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:117 -msgid "Select to display reviews on the page." -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:127 -msgid "Number of Reviews" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:128 -msgid "Number of reviews to display on the page." -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:137 -msgid "Prevent Multiple Reviews" -msgstr "" - -#: includes/admin/class.llms.admin.reviews.php:138 -msgid "Select to prevent a user from submitting more than one review." -msgstr "" - -#: includes/admin/class.llms.admin.settings.php:77 -msgid "Your settings have been saved." -msgstr "" - -#: includes/admin/class.llms.admin.settings.php:566 -msgid "Upload" -msgstr "" - -#: includes/admin/class.llms.admin.settings.php:605 -msgid "Select a page…" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:140 -msgid "Allow" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:142 -msgid "Install a Sample Course" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:144 -msgid "Save & Continue" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:157 -msgid "No thanks" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:159 -msgid "Skip this step" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:187 -msgid "Welcome!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:188, -#: includes/admin/class.llms.admin.setup.wizard.php:316 -msgid "Page Setup" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:189, -#: includes/admin/class.llms.admin.setup.wizard.php:350 -msgid "Payments" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:191 -msgid "Finish!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:236 -msgid "Skip setup" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:237 -msgid "Get Started Now" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:241 -msgid "Go back" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:249 -msgid "Start from Scratch" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:262 -msgid "Return to the WordPress Dashboard" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:285 -msgid "Help Improve LifterLMS & Get a Coupon" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:286 -msgid "" -"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!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:287 -msgid "Click \"Allow\" to and we'll send you a coupon immediately." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:288 -msgid "Find out more information" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:294 -msgid "Setup Complete!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:295 -msgid "Here's some resources to help you get familiar with LifterLMS:" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:297 -msgid "Watch the LifterLMS video tutorials" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:298 -msgid "Read the LifterLMS Getting Started Guide" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:301 -msgid "Get started with your first course" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:307 -msgid "Welcome to LifterLMS!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:309 -msgid "" -"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!" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:310 -msgid "" -"It will only take a few minutes and it is completely optional. If you don't " -"have the time now, come back later." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:318 -msgid "" -"LifterLMS has a few essential pages. The following will be created " -"automatically if they don't already exist." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:323 -msgid "" -"This page is where your visitors will find a list of all your available " -"courses." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:327, -#: includes/admin/settings/class.llms.settings.memberships.php:89 -msgid "" -"This page is where your visitors will find a list of all your available " -"memberships." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:330, -#: includes/admin/settings/class.llms.settings.checkout.php:30 -msgid "Checkout" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:331 -msgid "" -"This is the page where visitors will be directed in order to pay for courses " -"and memberships." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:334, -#: includes/admin/settings/class.llms.settings.accounts.php:57 -msgid "Student Dashboard" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:335, -#: includes/admin/settings/class.llms.settings.accounts.php:63 -msgid "" -"Page where students can view and manage their current enrollments, earned " -"certificates and achievements, account information, and purchase history." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:339 -msgid "" -"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." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:355 -msgid "Which country should be used as the default for student registrations?" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:367 -msgid "Which currency should be used for payment processing?" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:374 -msgid "If you currency is not listed you can %1$sadd it later%2$s." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:380 -msgid "" -"With LifterLMS you can accept both online and offline payments. Be sure to " -"install a %1$spayment gateway%2$s to accept online payments." -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:381 -msgid "Enable Offline Payments" -msgstr "" - -#: includes/admin/class.llms.admin.setup.wizard.php:482 -msgid "There was an error saving your data, please try again." -msgstr "" - -#: includes/admin/class.llms.admin.system-report.php:47 -msgid "Support" -msgstr "" - -#: includes/admin/class.llms.admin.system-report.php:52 -msgid "Copy for Support" -msgstr "" - -#: includes/admin/class.llms.admin.system-report.php:53 -msgid "Get Help" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:93, -#: includes/privacy/class-llms-privacy.php:102, -#: includes/privacy/class-llms-privacy.php:201, -#: includes/admin/reporting/tables/llms.table.students.php:454 -msgid "Billing Address 1" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:101, -#: includes/privacy/class-llms-privacy.php:103, -#: includes/privacy/class-llms-privacy.php:202, -#: includes/admin/reporting/tables/llms.table.students.php:459 -msgid "Billing Address 2" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:109, -#: includes/privacy/class-llms-privacy.php:104, -#: includes/privacy/class-llms-privacy.php:203, -#: includes/admin/reporting/tables/llms.table.students.php:464 -msgid "Billing City" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:117, -#: includes/privacy/class-llms-privacy.php:105, -#: includes/privacy/class-llms-privacy.php:204, -#: includes/admin/reporting/tables/llms.table.students.php:469 -msgid "Billing State" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:125, -#: includes/privacy/class-llms-privacy.php:106, -#: includes/privacy/class-llms-privacy.php:205 -msgid "Billing Zip Code" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:133, -#: includes/privacy/class-llms-privacy.php:107, -#: includes/privacy/class-llms-privacy.php:206, -#: includes/admin/reporting/tables/llms.table.students.php:479 -msgid "Billing Country" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:141, -#: includes/privacy/class-llms-privacy.php:108, -#: includes/privacy/class-llms-privacy.php:207, -#: includes/admin/reporting/tables/llms.table.students.php:484 -msgid "Phone" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:229 -msgid "Parent Instructor(s)" -msgstr "" - -#: includes/admin/class.llms.admin.user.custom.fields.php:330 -msgid "Required field \"%s\" is missing." -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:72 -msgid "Choose Course/Membership" -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:76, -#: includes/functions/llms.functions.updates.php:110, -#: includes/models/model.llms.access.plan.php:326 -msgid "Enroll" -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:102 -msgid "Please select a Course or Membership to enroll users into!" -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:114 -msgid "Please select users to enroll into <em>%s</em>." -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:166 -msgid "No such users found. Cannot enroll into <em>%s</em>." -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:221 -msgid "Failed to enroll <em>%1s</em> into <em>%2s</em>." -msgstr "" - -#: includes/admin/class.llms.student.bulk.enroll.php:221 -msgid "Successfully enrolled <em>%1s</em> into <em>%2s</em>." -msgstr "" - -#: includes/admin/llms.functions.admin.php:109, -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:68 -msgid "Site Title" -msgstr "" - -#: includes/admin/llms.functions.admin.php:110 -msgid "Site URL" -msgstr "" - -#: includes/admin/llms.functions.admin.php:111, -#: includes/privacy/class-llms-privacy-exporters.php:107, -#: includes/privacy/class-llms-privacy-exporters.php:146, -#: includes/admin/reporting/tables/llms.table.achievements.php:155, -#: includes/admin/reporting/tables/llms.table.certificates.php:158 -msgid "Earned Date" -msgstr "" - -#: includes/admin/llms.functions.admin.php:112, -#: includes/admin/llms.functions.admin.php:128 -msgid "Student First Name" -msgstr "" - -#: includes/admin/llms.functions.admin.php:113, -#: includes/admin/llms.functions.admin.php:129 -msgid "Student Last Name" -msgstr "" - -#: includes/admin/llms.functions.admin.php:114, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:40 -msgid "Student Email" -msgstr "" - -#: includes/admin/llms.functions.admin.php:115 -msgid "Student User ID" -msgstr "" - -#: includes/admin/llms.functions.admin.php:116, -#: includes/admin/llms.functions.admin.php:127 -msgid "Student Username" -msgstr "" - -#: includes/admin/llms.functions.admin.php:124 -msgid "Website Title" -msgstr "" - -#: includes/admin/llms.functions.admin.php:125 -msgid "Website URL" -msgstr "" - -#: includes/admin/llms.functions.admin.php:126 -msgid "Student Email Address" -msgstr "" - -#: includes/admin/llms.functions.admin.php:130 -msgid "Current Date" -msgstr "" - -#: includes/admin/llms.functions.admin.php:153 -msgid "Merge Codes" -msgstr "" - -#: includes/admin/llms.functions.admin.php:163 -msgid "No merge codes found." -msgstr "" - -#: includes/controllers/class.llms.controller.lesson.progression.php:73, -#: includes/controllers/class.llms.controller.lesson.progression.php:115 -msgid "An error occurred, please try again." -msgstr "" - -#: includes/controllers/class.llms.controller.lesson.progression.php:124 -msgid "%s is now incomplete." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:85, -#: includes/controllers/class.llms.controller.orders.php:91, -#: includes/shortcodes/class.llms.shortcode.checkout.php:171 -msgid "Could not locate an order to confirm." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:96 -msgid "Only pending orders can be confirmed." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:189 -msgid "You must agree to the %s to continue." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:195 -msgid "Missing an Access Plan ID." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:199 -msgid "Invalid Access Plan ID." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:231 -msgid "No payment method selected." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:262 -msgid "" -"An unknown error occurred when attempting to create an account, please try " -"again." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:267 -msgid "" -"You already have access to this %2$s! Visit your dashboard <a href=\"%s" -"\">here.</a>" -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:298 -msgid "There was an error creating your order, please try again." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:363 -msgid "" -"Student unenrolled at the end of access period due to subscription " -"cancellation." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:370 -msgid "Student unenrolled due to automatic access plan expiration" -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:457 -msgid "Missing order information." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:462, -#: templates/myaccount/view-order.php:10 -msgid "Invalid Order." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:464 -msgid "Missing gateway information." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:580 -msgid "Order status changed from %1$s to %2$s" -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:611 -msgid "The selected payment gateway is not currently enabled." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:616 -msgid "" -"%s does not support recurring payments and cannot process this transaction." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:621 -msgid "" -"%s does not support single payments and cannot process this transaction." -msgstr "" - -#: includes/controllers/class.llms.controller.orders.php:626 -msgid "An invalid payment method was selected." -msgstr "" - -#: includes/controllers/class.llms.controller.quizzes.php:34 -msgid "Could not proceed to the quiz because required information was missing." -msgstr "" - -#: includes/emails/class.llms.email.reset.password.php:26 -msgid "Password Reset for {site_title}" -msgstr "" - -#: includes/emails/class.llms.email.reset.password.php:27 -msgid "Reset Your Password" -msgstr "" - -#: includes/functions/llms.functions.access.php:185 -msgctxt "restricted by course prerequisite message" -msgid "" -"The lesson \"%1$s\" cannot be accessed until the required prerequisite " -"course \"%2$s\" is completed." -msgstr "" - -#: includes/functions/llms.functions.access.php:192 -msgctxt "restricted by course track prerequisite message" -msgid "" -"The lesson \"%1$s\" cannot be accessed until the required prerequisite track " -"\"%2$s\" is completed." -msgstr "" - -#: includes/functions/llms.functions.access.php:214 -msgctxt "lesson restricted by drip settings message" -msgid "The lesson \"%1$s\" will be available on %2$s" -msgstr "" - -#: includes/functions/llms.functions.access.php:221 -msgctxt "lesson restricted by prerequisite message" -msgid "" -"The lesson \"%1$s\" cannot be accessed until the required prerequisite \"%2$s" -"\" is completed." -msgstr "" - -#: includes/functions/llms.functions.currency.php:48 -msgid "Afghanistan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:49 -msgid "Albania" -msgstr "" - -#: includes/functions/llms.functions.currency.php:50 -msgid "Algeria" -msgstr "" - -#: includes/functions/llms.functions.currency.php:51 -msgid "American Samoa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:52 -msgid "Andorra" -msgstr "" - -#: includes/functions/llms.functions.currency.php:53 -msgid "Angola" -msgstr "" - -#: includes/functions/llms.functions.currency.php:54 -msgid "Anguilla" -msgstr "" - -#: includes/functions/llms.functions.currency.php:55 -msgid "Antarctica" -msgstr "" - -#: includes/functions/llms.functions.currency.php:56 -msgid "Antigua And Barbuda" -msgstr "" - -#: includes/functions/llms.functions.currency.php:57 -msgid "Argentina" -msgstr "" - -#: includes/functions/llms.functions.currency.php:58 -msgid "Armenia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:59 -msgid "Aruba" -msgstr "" - -#: includes/functions/llms.functions.currency.php:60 -msgid "Australia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:61 -msgid "Austria" -msgstr "" - -#: includes/functions/llms.functions.currency.php:62 -msgid "Azerbaijan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:63 -msgid "Bahamas" -msgstr "" - -#: includes/functions/llms.functions.currency.php:64 -msgid "Bahrain" -msgstr "" - -#: includes/functions/llms.functions.currency.php:65 -msgid "Bangladesh" -msgstr "" - -#: includes/functions/llms.functions.currency.php:66 -msgid "Barbados" -msgstr "" - -#: includes/functions/llms.functions.currency.php:67 -msgid "Belarus" -msgstr "" - -#: includes/functions/llms.functions.currency.php:68 -msgid "Belgium" -msgstr "" - -#: includes/functions/llms.functions.currency.php:69 -msgid "Belize" -msgstr "" - -#: includes/functions/llms.functions.currency.php:70 -msgid "Benin" -msgstr "" - -#: includes/functions/llms.functions.currency.php:71 -msgid "Bermuda" -msgstr "" - -#: includes/functions/llms.functions.currency.php:72 -msgid "Bhutan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:73 -msgid "Bolivia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:74 -msgid "Bosnia And Herzegowina" -msgstr "" - -#: includes/functions/llms.functions.currency.php:75 -msgid "Botswana" -msgstr "" - -#: includes/functions/llms.functions.currency.php:76 -msgid "Bouvet Island" -msgstr "" - -#: includes/functions/llms.functions.currency.php:77 -msgid "Brazil" -msgstr "" - -#: includes/functions/llms.functions.currency.php:78 -msgid "British Indian Ocean Territory" -msgstr "" - -#: includes/functions/llms.functions.currency.php:79 -msgid "Brunei Darussalam" -msgstr "" - -#: includes/functions/llms.functions.currency.php:80 -msgid "Bulgaria" -msgstr "" - -#: includes/functions/llms.functions.currency.php:81 -msgid "Burkina Faso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:82 -msgid "Burundi" -msgstr "" - -#: includes/functions/llms.functions.currency.php:83 -msgid "Cambodia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:84 -msgid "Cameroon" -msgstr "" - -#: includes/functions/llms.functions.currency.php:85 -msgid "Canada" -msgstr "" - -#: includes/functions/llms.functions.currency.php:86 -msgid "Cape Verde" -msgstr "" - -#: includes/functions/llms.functions.currency.php:87 -msgid "Cayman Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:88 -msgid "Central African Republic" -msgstr "" - -#: includes/functions/llms.functions.currency.php:89 -msgid "Chad" -msgstr "" - -#: includes/functions/llms.functions.currency.php:90 -msgid "Chile" -msgstr "" - -#: includes/functions/llms.functions.currency.php:91 -msgid "China" -msgstr "" - -#: includes/functions/llms.functions.currency.php:92 -msgid "Christmas Island" -msgstr "" - -#: includes/functions/llms.functions.currency.php:93 -msgid "Cocos (Keeling) Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:94 -msgid "Colombia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:95 -msgid "Comoros" -msgstr "" - -#: includes/functions/llms.functions.currency.php:96 -msgid "Congo" -msgstr "" - -#: includes/functions/llms.functions.currency.php:97 -msgid "Congo, The Democratic Republic Of The" -msgstr "" - -#: includes/functions/llms.functions.currency.php:98 -msgid "Cook Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:99 -msgid "Costa Rica" -msgstr "" - -#: includes/functions/llms.functions.currency.php:100 -msgid "Cote D'Ivoire" -msgstr "" - -#: includes/functions/llms.functions.currency.php:101 -msgid "Croatia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:102 -msgid "Cuba" -msgstr "" - -#: includes/functions/llms.functions.currency.php:103 -msgid "Cyprus" -msgstr "" - -#: includes/functions/llms.functions.currency.php:104 -msgid "Czech Republic" -msgstr "" - -#: includes/functions/llms.functions.currency.php:105 -msgid "Denmark" -msgstr "" - -#: includes/functions/llms.functions.currency.php:106 -msgid "Djibouti" -msgstr "" - -#: includes/functions/llms.functions.currency.php:107 -msgid "Dominica" -msgstr "" - -#: includes/functions/llms.functions.currency.php:108 -msgid "Dominican Republic" -msgstr "" - -#: includes/functions/llms.functions.currency.php:109 -msgid "East Timor" -msgstr "" - -#: includes/functions/llms.functions.currency.php:110 -msgid "Ecuador" -msgstr "" - -#: includes/functions/llms.functions.currency.php:111 -msgid "Egypt" -msgstr "" - -#: includes/functions/llms.functions.currency.php:112 -msgid "El Salvador" -msgstr "" - -#: includes/functions/llms.functions.currency.php:113 -msgid "Equatorial Guinea" -msgstr "" - -#: includes/functions/llms.functions.currency.php:114 -msgid "Eritrea" -msgstr "" - -#: includes/functions/llms.functions.currency.php:115 -msgid "Estonia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:116 -msgid "Ethiopia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:117 -msgid "Falkland Islands (Malvinas)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:118 -msgid "Faroe Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:119 -msgid "Fiji" -msgstr "" - -#: includes/functions/llms.functions.currency.php:120 -msgid "Finland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:121 -msgid "France" -msgstr "" - -#: includes/functions/llms.functions.currency.php:122 -msgid "France, Metropolitan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:123 -msgid "French Guiana" -msgstr "" - -#: includes/functions/llms.functions.currency.php:124 -msgid "French Polynesia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:125 -msgid "French Southern Territories" -msgstr "" - -#: includes/functions/llms.functions.currency.php:126 -msgid "Gabon" -msgstr "" - -#: includes/functions/llms.functions.currency.php:127 -msgid "Gambia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:128 -msgid "Georgia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:129 -msgid "Germany" -msgstr "" - -#: includes/functions/llms.functions.currency.php:130 -msgid "Ghana" -msgstr "" - -#: includes/functions/llms.functions.currency.php:131 -msgid "Gibraltar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:132 -msgid "Greece" -msgstr "" - -#: includes/functions/llms.functions.currency.php:133 -msgid "Greenland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:134 -msgid "Grenada" -msgstr "" - -#: includes/functions/llms.functions.currency.php:135 -msgid "Guadeloupe" -msgstr "" - -#: includes/functions/llms.functions.currency.php:136 -msgid "Guam" -msgstr "" - -#: includes/functions/llms.functions.currency.php:137 -msgid "Guatemala" -msgstr "" - -#: includes/functions/llms.functions.currency.php:138 -msgid "Guinea" -msgstr "" - -#: includes/functions/llms.functions.currency.php:139 -msgid "Guinea-Bissau" -msgstr "" - -#: includes/functions/llms.functions.currency.php:140 -msgid "Guyana" -msgstr "" - -#: includes/functions/llms.functions.currency.php:141 -msgid "Haiti" -msgstr "" - -#: includes/functions/llms.functions.currency.php:142 -msgid "Heard And Mc Donald Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:143 -msgid "Holy See (Vatican City State)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:144 -msgid "Honduras" -msgstr "" - -#: includes/functions/llms.functions.currency.php:145 -msgid "Hong Kong" -msgstr "" - -#: includes/functions/llms.functions.currency.php:146 -msgid "Hungary" -msgstr "" - -#: includes/functions/llms.functions.currency.php:147 -msgid "Iceland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:148 -msgid "India" -msgstr "" - -#: includes/functions/llms.functions.currency.php:149 -msgid "Indonesia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:150 -msgid "Iran (Islamic Republic Of)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:151 -msgid "Iraq" -msgstr "" - -#: includes/functions/llms.functions.currency.php:152 -msgid "Ireland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:153 -msgid "Israel" -msgstr "" - -#: includes/functions/llms.functions.currency.php:154 -msgid "Italy" -msgstr "" - -#: includes/functions/llms.functions.currency.php:155 -msgid "Jamaica" -msgstr "" - -#: includes/functions/llms.functions.currency.php:156 -msgid "Japan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:157 -msgid "Jordan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:158 -msgid "Kazakhstan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:159 -msgid "Kenya" -msgstr "" - -#: includes/functions/llms.functions.currency.php:160 -msgid "Kiribati" -msgstr "" - -#: includes/functions/llms.functions.currency.php:161 -msgid "Korea, Democratic People's Republic Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:162 -msgid "Korea, Republic Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:163 -msgid "Kuwait" -msgstr "" - -#: includes/functions/llms.functions.currency.php:164 -msgid "Kyrgyzstan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:165 -msgid "Lao People's Democratic Republic" -msgstr "" - -#: includes/functions/llms.functions.currency.php:166 -msgid "Latvia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:167 -msgid "Lebanon" -msgstr "" - -#: includes/functions/llms.functions.currency.php:168 -msgid "Lesotho" -msgstr "" - -#: includes/functions/llms.functions.currency.php:169 -msgid "Liberia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:170 -msgid "Libyan Arab Jamahiriya" -msgstr "" - -#: includes/functions/llms.functions.currency.php:171 -msgid "Liechtenstein" -msgstr "" - -#: includes/functions/llms.functions.currency.php:172 -msgid "Lithuania" -msgstr "" - -#: includes/functions/llms.functions.currency.php:173 -msgid "Luxembourg" -msgstr "" - -#: includes/functions/llms.functions.currency.php:174 -msgid "Macau" -msgstr "" - -#: includes/functions/llms.functions.currency.php:175 -msgid "Macedonia, Former Yugoslav Republic Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:176 -msgid "Madagascar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:177 -msgid "Malawi" -msgstr "" - -#: includes/functions/llms.functions.currency.php:178 -msgid "Malaysia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:179 -msgid "Maldives" -msgstr "" - -#: includes/functions/llms.functions.currency.php:180 -msgid "Mali" -msgstr "" - -#: includes/functions/llms.functions.currency.php:181 -msgid "Malta" -msgstr "" - -#: includes/functions/llms.functions.currency.php:182 -msgid "Marshall Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:183 -msgid "Martinique" -msgstr "" - -#: includes/functions/llms.functions.currency.php:184 -msgid "Mauritania" -msgstr "" - -#: includes/functions/llms.functions.currency.php:185 -msgid "Mauritius" -msgstr "" - -#: includes/functions/llms.functions.currency.php:186 -msgid "Mayotte" -msgstr "" - -#: includes/functions/llms.functions.currency.php:187 -msgid "Mexico" -msgstr "" - -#: includes/functions/llms.functions.currency.php:188 -msgid "Micronesia, Federated States Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:189 -msgid "Moldova, Republic Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:190 -msgid "Monaco" -msgstr "" - -#: includes/functions/llms.functions.currency.php:191 -msgid "Mongolia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:192 -msgid "Montserrat" -msgstr "" - -#: includes/functions/llms.functions.currency.php:193 -msgid "Morocco" -msgstr "" - -#: includes/functions/llms.functions.currency.php:194 -msgid "Mozambique" -msgstr "" - -#: includes/functions/llms.functions.currency.php:195 -msgid "Myanmar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:196 -msgid "Namibia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:197 -msgid "Nauru" -msgstr "" - -#: includes/functions/llms.functions.currency.php:198 -msgid "Nepal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:199 -msgid "Netherlands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:200 -msgid "Netherlands Antilles" -msgstr "" - -#: includes/functions/llms.functions.currency.php:201 -msgid "New Caledonia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:202 -msgid "New Zealand" -msgstr "" - -#: includes/functions/llms.functions.currency.php:203 -msgid "Nicaragua" -msgstr "" - -#: includes/functions/llms.functions.currency.php:204 -msgid "Niger" -msgstr "" - -#: includes/functions/llms.functions.currency.php:205 -msgid "Nigeria" -msgstr "" - -#: includes/functions/llms.functions.currency.php:206 -msgid "Niue" -msgstr "" - -#: includes/functions/llms.functions.currency.php:207 -msgid "Norfolk Island" -msgstr "" - -#: includes/functions/llms.functions.currency.php:208 -msgid "Northern Mariana Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:209 -msgid "Norway" -msgstr "" - -#: includes/functions/llms.functions.currency.php:210 -msgid "Oman" -msgstr "" - -#: includes/functions/llms.functions.currency.php:211 -msgid "Pakistan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:212 -msgid "Palau" -msgstr "" - -#: includes/functions/llms.functions.currency.php:213 -msgid "Panama" -msgstr "" - -#: includes/functions/llms.functions.currency.php:214 -msgid "Papua New Guinea" -msgstr "" - -#: includes/functions/llms.functions.currency.php:215 -msgid "Paraguay" -msgstr "" - -#: includes/functions/llms.functions.currency.php:216 -msgid "Peru" -msgstr "" - -#: includes/functions/llms.functions.currency.php:217 -msgid "Philippines" -msgstr "" - -#: includes/functions/llms.functions.currency.php:218 -msgid "Pitcairn" -msgstr "" - -#: includes/functions/llms.functions.currency.php:219 -msgid "Poland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:220 -msgid "Portugal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:221 -msgid "Puerto Rico" -msgstr "" - -#: includes/functions/llms.functions.currency.php:222 -msgid "Qatar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:223 -msgid "Reunion" -msgstr "" - -#: includes/functions/llms.functions.currency.php:224 -msgid "Romania" -msgstr "" - -#: includes/functions/llms.functions.currency.php:225 -msgid "Russian Federation" -msgstr "" - -#: includes/functions/llms.functions.currency.php:226 -msgid "Rwanda" -msgstr "" - -#: includes/functions/llms.functions.currency.php:227 -msgid "Saint Kitts And Nevis" -msgstr "" - -#: includes/functions/llms.functions.currency.php:228 -msgid "Saint Lucia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:229 -msgid "Saint Vincent And The Grenadines" -msgstr "" - -#: includes/functions/llms.functions.currency.php:230 -msgid "Samoa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:231 -msgid "San Marino" -msgstr "" - -#: includes/functions/llms.functions.currency.php:232 -msgid "Sao Tome And Principe" -msgstr "" - -#: includes/functions/llms.functions.currency.php:233 -msgid "Saudi Arabia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:234 -msgid "Senegal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:235 -msgid "Seychelles" -msgstr "" - -#: includes/functions/llms.functions.currency.php:236 -msgid "Sierra Leone" -msgstr "" - -#: includes/functions/llms.functions.currency.php:237 -msgid "Singapore" -msgstr "" - -#: includes/functions/llms.functions.currency.php:238 -msgid "Slovakia (Slovak Republic)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:239 -msgid "Slovenia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:240 -msgid "Solomon Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:241 -msgid "Somalia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:242 -msgid "South Africa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:243 -msgid "South Georgia, South Sandwich Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:244 -msgid "Spain" -msgstr "" - -#: includes/functions/llms.functions.currency.php:245 -msgid "Sri Lanka" -msgstr "" - -#: includes/functions/llms.functions.currency.php:246 -msgid "St. Helena" -msgstr "" - -#: includes/functions/llms.functions.currency.php:247 -msgid "St. Pierre And Miquelon" -msgstr "" - -#: includes/functions/llms.functions.currency.php:248 -msgid "Sudan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:249 -msgid "Suriname" -msgstr "" - -#: includes/functions/llms.functions.currency.php:250 -msgid "Svalbard And Jan Mayen Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:251 -msgid "Swaziland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:252 -msgid "Sweden" -msgstr "" - -#: includes/functions/llms.functions.currency.php:253 -msgid "Switzerland" -msgstr "" - -#: includes/functions/llms.functions.currency.php:254 -msgid "Syrian Arab Republic" -msgstr "" - -#: includes/functions/llms.functions.currency.php:255 -msgid "Taiwan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:256 -msgid "Tajikistan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:257 -msgid "Tanzania, United Republic Of" -msgstr "" - -#: includes/functions/llms.functions.currency.php:258 -msgid "Thailand" -msgstr "" - -#: includes/functions/llms.functions.currency.php:259 -msgid "Togo" -msgstr "" - -#: includes/functions/llms.functions.currency.php:260 -msgid "Tokelau" -msgstr "" - -#: includes/functions/llms.functions.currency.php:261 -msgid "Tonga" -msgstr "" - -#: includes/functions/llms.functions.currency.php:262 -msgid "Trinidad And Tobago" -msgstr "" - -#: includes/functions/llms.functions.currency.php:263 -msgid "Tunisia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:264 -msgid "Turkey" -msgstr "" - -#: includes/functions/llms.functions.currency.php:265 -msgid "Turkmenistan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:266 -msgid "Turks And Caicos Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:267 -msgid "Tuvalu" -msgstr "" - -#: includes/functions/llms.functions.currency.php:268 -msgid "Uganda" -msgstr "" - -#: includes/functions/llms.functions.currency.php:269 -msgid "Ukraine" -msgstr "" - -#: includes/functions/llms.functions.currency.php:270 -msgid "United Arab Emirates" -msgstr "" - -#: includes/functions/llms.functions.currency.php:271 -msgid "United Kingdom" -msgstr "" - -#: includes/functions/llms.functions.currency.php:272 -msgid "United States" -msgstr "" - -#: includes/functions/llms.functions.currency.php:273 -msgid "United States Minor Outlying Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:274 -msgid "Uruguay" -msgstr "" - -#: includes/functions/llms.functions.currency.php:275 -msgid "Uzbekistan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:276 -msgid "Vanuatu" -msgstr "" - -#: includes/functions/llms.functions.currency.php:277 -msgid "Venezuela" -msgstr "" - -#: includes/functions/llms.functions.currency.php:278 -msgid "Viet Nam" -msgstr "" - -#: includes/functions/llms.functions.currency.php:279 -msgid "Virgin Islands (British)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:280 -msgid "Virgin Islands (U.S.)" -msgstr "" - -#: includes/functions/llms.functions.currency.php:281 -msgid "Wallis And Futuna Islands" -msgstr "" - -#: includes/functions/llms.functions.currency.php:282 -msgid "Western Sahara" -msgstr "" - -#: includes/functions/llms.functions.currency.php:283 -msgid "Yemen" -msgstr "" - -#: includes/functions/llms.functions.currency.php:284 -msgid "Yugoslavia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:285 -msgid "Zambia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:286 -msgid "Zimbabwe" -msgstr "" - -#: includes/functions/llms.functions.currency.php:341 -msgid "United Arab Emirates dirham" -msgstr "" - -#: includes/functions/llms.functions.currency.php:342 -msgid "Afghan afghani" -msgstr "" - -#: includes/functions/llms.functions.currency.php:343 -msgid "Albanian lek" -msgstr "" - -#: includes/functions/llms.functions.currency.php:344 -msgid "Armenian dram" -msgstr "" - -#: includes/functions/llms.functions.currency.php:345 -msgid "Netherlands Antillean guilder" -msgstr "" - -#: includes/functions/llms.functions.currency.php:346 -msgid "Angolan kwanza" -msgstr "" - -#: includes/functions/llms.functions.currency.php:347 -msgid "Argentine peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:348 -msgid "Australian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:349 -msgid "Aruban florin" -msgstr "" - -#: includes/functions/llms.functions.currency.php:350 -msgid "Azerbaijani manat" -msgstr "" - -#: includes/functions/llms.functions.currency.php:351 -msgid "Bosnia and Herzegovina convertible mark" -msgstr "" - -#: includes/functions/llms.functions.currency.php:352 -msgid "Barbadian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:353 -msgid "Bangladeshi taka" -msgstr "" - -#: includes/functions/llms.functions.currency.php:354 -msgid "Bulgarian lev" -msgstr "" - -#: includes/functions/llms.functions.currency.php:355 -msgid "Bahraini dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:356 -msgid "Burundian franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:357 -msgid "Bermudian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:358 -msgid "Brunei dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:359 -msgid "Bolivian boliviano" -msgstr "" - -#: includes/functions/llms.functions.currency.php:360 -msgid "Brazilian real" -msgstr "" - -#: includes/functions/llms.functions.currency.php:361 -msgid "Bahamian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:362 -msgid "Bitcoin" -msgstr "" - -#: includes/functions/llms.functions.currency.php:363 -msgid "Bhutanese ngultrum" -msgstr "" - -#: includes/functions/llms.functions.currency.php:364 -msgid "Botswana pula" -msgstr "" - -#: includes/functions/llms.functions.currency.php:365 -msgid "Belarusian ruble" -msgstr "" - -#: includes/functions/llms.functions.currency.php:366 -msgid "Belize dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:367 -msgid "Canadian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:368 -msgid "Congolese franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:369 -msgid "Swiss franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:370 -msgid "Chilean peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:371 -msgid "Chinese yuan" -msgstr "" - -#: includes/functions/llms.functions.currency.php:372 -msgid "Colombian peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:373 -msgid "Costa Rican colón" -msgstr "" - -#: includes/functions/llms.functions.currency.php:374 -msgid "Cuban convertible peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:375 -msgid "Cuban peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:376 -msgid "Cape Verdean escudo" -msgstr "" - -#: includes/functions/llms.functions.currency.php:377 -msgid "Czech koruna" -msgstr "" - -#: includes/functions/llms.functions.currency.php:378 -msgid "Djiboutian franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:379 -msgid "Danish krone" -msgstr "" - -#: includes/functions/llms.functions.currency.php:380 -msgid "Dominican peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:381 -msgid "Algerian dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:382 -msgid "Egyptian pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:383 -msgid "Eritrean nakfa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:384 -msgid "Ethiopian birr" -msgstr "" - -#: includes/functions/llms.functions.currency.php:385 -msgid "Euro" -msgstr "" - -#: includes/functions/llms.functions.currency.php:386 -msgid "Fijian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:387 -msgid "Falkland Islands pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:388 -msgid "Pound sterling" -msgstr "" - -#: includes/functions/llms.functions.currency.php:389 -msgid "Georgian lari" -msgstr "" - -#: includes/functions/llms.functions.currency.php:390 -msgid "Guernsey pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:391 -msgid "Ghana cedi" -msgstr "" - -#: includes/functions/llms.functions.currency.php:392 -msgid "Gibraltar pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:393 -msgid "Gambian dalasi" -msgstr "" - -#: includes/functions/llms.functions.currency.php:394 -msgid "Guinean franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:395 -msgid "Guatemalan quetzal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:396 -msgid "Guyanese dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:397 -msgid "Hong Kong dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:398 -msgid "Honduran lempira" -msgstr "" - -#: includes/functions/llms.functions.currency.php:399 -msgid "Croatian kuna" -msgstr "" - -#: includes/functions/llms.functions.currency.php:400 -msgid "Haitian gourde" -msgstr "" - -#: includes/functions/llms.functions.currency.php:401 -msgid "Hungarian forint" -msgstr "" - -#: includes/functions/llms.functions.currency.php:402 -msgid "Indonesian rupiah" -msgstr "" - -#: includes/functions/llms.functions.currency.php:403 -msgid "Israeli new shekel" -msgstr "" - -#: includes/functions/llms.functions.currency.php:404 -msgid "Manx pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:405 -msgid "Indian rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:406 -msgid "Iraqi dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:407 -msgid "Iranian rial" -msgstr "" - -#: includes/functions/llms.functions.currency.php:408 -msgid "Icelandic króna" -msgstr "" - -#: includes/functions/llms.functions.currency.php:409 -msgid "Jersey pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:410 -msgid "Jamaican dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:411 -msgid "Jordanian dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:412 -msgid "Japanese yen" -msgstr "" - -#: includes/functions/llms.functions.currency.php:413 -msgid "Kenyan shilling" -msgstr "" - -#: includes/functions/llms.functions.currency.php:414 -msgid "Kyrgyzstani som" -msgstr "" - -#: includes/functions/llms.functions.currency.php:415 -msgid "Cambodian riel" -msgstr "" - -#: includes/functions/llms.functions.currency.php:416 -msgid "Comorian franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:417 -msgid "North Korean won" -msgstr "" - -#: includes/functions/llms.functions.currency.php:418 -msgid "South Korean won" -msgstr "" - -#: includes/functions/llms.functions.currency.php:419 -msgid "Kuwaiti dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:420 -msgid "Cayman Islands dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:421 -msgid "Kazakhstani tenge" -msgstr "" - -#: includes/functions/llms.functions.currency.php:422 -msgid "Lao kip" -msgstr "" - -#: includes/functions/llms.functions.currency.php:423 -msgid "Lebanese pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:424 -msgid "Sri Lankan rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:425 -msgid "Liberian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:426 -msgid "Lesotho loti" -msgstr "" - -#: includes/functions/llms.functions.currency.php:427 -msgid "Libyan dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:428 -msgid "Moroccan dirham" -msgstr "" - -#: includes/functions/llms.functions.currency.php:429 -msgid "Moldovan leu" -msgstr "" - -#: includes/functions/llms.functions.currency.php:430 -msgid "Malagasy ariary" -msgstr "" - -#: includes/functions/llms.functions.currency.php:431 -msgid "Macedonian denar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:432 -msgid "Burmese kyat" -msgstr "" - -#: includes/functions/llms.functions.currency.php:433 -msgid "Mongolian tögrög" -msgstr "" - -#: includes/functions/llms.functions.currency.php:434 -msgid "Macanese pataca" -msgstr "" - -#: includes/functions/llms.functions.currency.php:435 -msgid "Mauritanian ouguiya" -msgstr "" - -#: includes/functions/llms.functions.currency.php:436 -msgid "Mauritian rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:437 -msgid "Maldivian rufiyaa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:438 -msgid "Malawian kwacha" -msgstr "" - -#: includes/functions/llms.functions.currency.php:439 -msgid "Mexican peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:440 -msgid "Malaysian ringgit" -msgstr "" - -#: includes/functions/llms.functions.currency.php:441 -msgid "Mozambican metical" -msgstr "" - -#: includes/functions/llms.functions.currency.php:442 -msgid "Namibian dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:443 -msgid "Nigerian naira" -msgstr "" - -#: includes/functions/llms.functions.currency.php:444 -msgid "Nicaraguan córdoba" -msgstr "" - -#: includes/functions/llms.functions.currency.php:445 -msgid "Norwegian krone" -msgstr "" - -#: includes/functions/llms.functions.currency.php:446 -msgid "Nepalese rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:447 -msgid "New Zealand dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:448 -msgid "Omani rial" -msgstr "" - -#: includes/functions/llms.functions.currency.php:449 -msgid "Panamanian balboa" -msgstr "" - -#: includes/functions/llms.functions.currency.php:450 -msgid "Peruvian nuevo sol" -msgstr "" - -#: includes/functions/llms.functions.currency.php:451 -msgid "Papua New Guinean kina" -msgstr "" - -#: includes/functions/llms.functions.currency.php:452 -msgid "Philippine peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:453 -msgid "Pakistani rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:454 -msgid "Polish złoty" -msgstr "" - -#: includes/functions/llms.functions.currency.php:455 -msgid "Transnistrian ruble" -msgstr "" - -#: includes/functions/llms.functions.currency.php:456 -msgid "Paraguayan guaraní" -msgstr "" - -#: includes/functions/llms.functions.currency.php:457 -msgid "Qatari riyal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:458 -msgid "Romanian leu" -msgstr "" - -#: includes/functions/llms.functions.currency.php:459 -msgid "Serbian dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:460 -msgid "Russian ruble" -msgstr "" - -#: includes/functions/llms.functions.currency.php:461 -msgid "Rwandan franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:462 -msgid "Saudi riyal" -msgstr "" - -#: includes/functions/llms.functions.currency.php:463 -msgid "Solomon Islands dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:464 -msgid "Seychellois rupee" -msgstr "" - -#: includes/functions/llms.functions.currency.php:465 -msgid "Sudanese pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:466 -msgid "Swedish krona" -msgstr "" - -#: includes/functions/llms.functions.currency.php:467 -msgid "Singapore dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:468 -msgid "Saint Helena pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:469 -msgid "Sierra Leonean leone" -msgstr "" - -#: includes/functions/llms.functions.currency.php:470 -msgid "Somali shilling" -msgstr "" - -#: includes/functions/llms.functions.currency.php:471 -msgid "Surinamese dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:472 -msgid "South Sudanese pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:473 -msgid "São Tomé and Príncipe dobra" -msgstr "" - -#: includes/functions/llms.functions.currency.php:474 -msgid "Syrian pound" -msgstr "" - -#: includes/functions/llms.functions.currency.php:475 -msgid "Swazi lilangeni" -msgstr "" - -#: includes/functions/llms.functions.currency.php:476 -msgid "Thai baht" -msgstr "" - -#: includes/functions/llms.functions.currency.php:477 -msgid "Tajikistani somoni" -msgstr "" - -#: includes/functions/llms.functions.currency.php:478 -msgid "Turkmenistan manat" -msgstr "" - -#: includes/functions/llms.functions.currency.php:479 -msgid "Tunisian dinar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:480 -msgid "Tongan paʻanga" -msgstr "" - -#: includes/functions/llms.functions.currency.php:481 -msgid "Turkish lira" -msgstr "" - -#: includes/functions/llms.functions.currency.php:482 -msgid "Trinidad and Tobago dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:483 -msgid "New Taiwan dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:484 -msgid "Tanzanian shilling" -msgstr "" - -#: includes/functions/llms.functions.currency.php:485 -msgid "Ukrainian hryvnia" -msgstr "" - -#: includes/functions/llms.functions.currency.php:486 -msgid "Ugandan shilling" -msgstr "" - -#: includes/functions/llms.functions.currency.php:487 -msgid "United States dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:488 -msgid "Uruguayan peso" -msgstr "" - -#: includes/functions/llms.functions.currency.php:489 -msgid "Uzbekistani som" -msgstr "" - -#: includes/functions/llms.functions.currency.php:490 -msgid "Venezuelan bolívar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:491 -msgid "Vietnamese đồng" -msgstr "" - -#: includes/functions/llms.functions.currency.php:492 -msgid "Vanuatu vatu" -msgstr "" - -#: includes/functions/llms.functions.currency.php:493 -msgid "Samoan tālā" -msgstr "" - -#: includes/functions/llms.functions.currency.php:494 -msgid "Central African CFA franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:495 -msgid "East Caribbean dollar" -msgstr "" - -#: includes/functions/llms.functions.currency.php:496 -msgid "West African CFA franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:497 -msgid "CFP franc" -msgstr "" - -#: includes/functions/llms.functions.currency.php:498 -msgid "Yemeni rial" -msgstr "" - -#: includes/functions/llms.functions.currency.php:499 -msgid "South African rand" -msgstr "" - -#: includes/functions/llms.functions.currency.php:500 -msgid "Zambian kwacha" -msgstr "" - -#: includes/functions/llms.functions.person.php:203 -msgid "strong" -msgstr "" - -#: includes/functions/llms.functions.person.php:207 -msgid "medium" -msgstr "" - -#: includes/functions/llms.functions.person.php:211 -msgid "weak" -msgstr "" - -#: includes/functions/llms.functions.person.php:215 -msgid "very weak" -msgstr "" - -#: includes/functions/llms.functions.person.php:439 -msgid "Last Login" -msgstr "" - -#: includes/functions/llms.functions.person.php:440, -#: includes/integrations/class.llms.integration.buddypress.php:74, -#: includes/admin/analytics/class.llms.analytics.memberships.php:19, -#: includes/admin/analytics/class.llms.analytics.sales.php:114, -#: includes/admin/settings/class.llms.settings.memberships.php:19, -#: templates/admin/analytics/analytics.php:102, -#: templates/admin/post-types/product-access-plan.php:217, -#: templates/admin/reporting/nav-filters.php:84, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:67, -#: includes/admin/reporting/tables/llms.table.students.php:449, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:84 -msgid "Memberships" -msgstr "" - -#: includes/functions/llms.functions.person.php:467 -msgid "Never" -msgstr "" - -#: includes/functions/llms.functions.person.php:507 -msgid "No memberships" -msgstr "" - -#: includes/functions/llms.functions.privacy.php:39 -msgid "" -"Your personal data will be used to process your enrollment, support your " -"experience on this website, and for other purposes described in our " -"{{policy}}." -msgstr "" - -#: includes/functions/llms.functions.privacy.php:86 -msgid "I have read and agree to the {{terms}}." -msgstr "" - -#: includes/functions/llms.functions.quiz.php:95 -msgid "Incomplete" -msgstr "" - -#: includes/functions/llms.functions.quiz.php:96 -msgid "Pending Review" -msgstr "" - -#: includes/functions/llms.functions.quiz.php:97 -msgid "Fail" -msgstr "" - -#: includes/functions/llms.functions.quiz.php:98 -msgid "Pass" -msgstr "" - -#: includes/functions/llms.functions.quiz.php:115 -msgid "Layout" -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:31 -msgid "You are not enrolled in any courses." -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:102 -msgid "You are not enrolled in any memberships." -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:173 -msgid "View All My Achievements" -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:211 -msgid "View All My Certificates" -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:247 -msgid "View All My Courses" -msgstr "" - -#: includes/functions/llms.functions.templates.dashboard.php:283 -msgid "View All My Memberships" -msgstr "" - -#: includes/functions/llms.functions.updates.php:110, -#: includes/models/model.llms.access.plan.php:330 -msgid "Join" -msgstr "" - -#: includes/functions/llms.functions.updates.php:172 -msgid "Members Only" -msgstr "" - -#: includes/functions/llms.functions.updates.php:197 -msgid "One-Time Payment" -msgstr "" - -#: includes/functions/llms.functions.updates.php:241 -msgid "Subscription" -msgstr "" - -#: includes/functions/llms.functions.updates.php:458, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:259 -msgid "" -"This course opens on [lifterlms_course_info id=\"%d\" key=\"start_date\"]." -msgstr "" - -#: includes/functions/llms.functions.updates.php:459, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:269 -msgid "" -"This course closed on [lifterlms_course_info id=\"%d\" key=\"end_date\"]." -msgstr "" - -#: includes/functions/llms.functions.updates.php:472, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:336 -msgid "" -"Enrollment has closed because the maximum number of allowed students has " -"been reached." -msgstr "" - -#: includes/functions/llms.functions.updates.php:673 -msgid "" -"This order was migrated to the LifterLMS 3.0 data structure. %1$sLearn more" -"%2$s." -msgstr "" - -#: includes/functions/llms.functions.updates.php:978 -msgid "" -"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." -msgstr "" - -#: includes/functions/llms.functions.updates.php:1613 -msgid "" -"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." -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:33, -#: includes/integrations/class.llms.integration.bbpress.php:104 -msgid "bbPress" -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:34 -msgid "" -"Restrict forums and topics to memberships, add forums to courses, and " -"%1$smore%2$s." -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:111 -msgid "Select forums" -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:113 -msgid "" -"Add forums which will only be available to students currently enrolled in " -"this course." -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:117 -msgid "Private Course Forums" -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:167 -msgid "You must be enrolled in this course to access the course forum" -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:347 -msgid "Student creates a new forum topic" -msgstr "" - -#: includes/integrations/class.llms.integration.bbpress.php:348 -msgid "Student creates a new forum reply" -msgstr "" - -#: includes/integrations/class.llms.integration.buddypress.php:29 -msgid "BuddyPress" -msgstr "" - -#: includes/integrations/class.llms.integration.buddypress.php:30 -msgid "" -"Add LifterLMS information to user profiles and enable membership " -"restrictions for activity, group, and member directories. %1$sLearn More%2$s." -msgstr "" - -#: includes/models/model.llms.access.plan.php:95 -msgctxt "Access plan period" -msgid "year" -msgid_plural "years" -msgstr[0] "" -msgstr[1] "" - -#: includes/models/model.llms.access.plan.php:99 -msgctxt "Access plan period" -msgid "month" -msgid_plural "months" -msgstr[0] "" -msgstr[1] "" - -#: includes/models/model.llms.access.plan.php:103 -msgctxt "Access plan period" -msgid "week" -msgid_plural "weeks" -msgstr[0] "" -msgstr[1] "" - -#: includes/models/model.llms.access.plan.php:107 -msgctxt "Access plan period" -msgid "day" -msgid_plural "days" -msgstr[0] "" -msgstr[1] "" - -#: includes/models/model.llms.access.plan.php:175, -#: includes/models/model.llms.lesson.php:273 -msgid "FREE" -msgstr "" - -#: includes/models/model.llms.access.plan.php:334 -msgid "Buy" -msgstr "" - -#: includes/models/model.llms.access.plan.php:354 -msgctxt "Access expiration date" -msgid "access until %s" -msgstr "" - -#: includes/models/model.llms.access.plan.php:360 -msgctxt "Access period description" -msgid "%1$d %2$s of access" -msgstr "" - -#: includes/models/model.llms.access.plan.php:390 -msgctxt "subscription schedule" -msgid "per %s" -msgstr "" - -#: includes/models/model.llms.access.plan.php:398 -msgctxt "subscription schedule" -msgid "every %1$d %2$s" -msgstr "" - -#: includes/models/model.llms.access.plan.php:407 -msgctxt "subscription # of payments" -msgid "for %1$d total payments" -msgstr "" - -#: includes/models/model.llms.access.plan.php:430 -msgctxt "trial offer description" -msgid "for %1$d %2$s" -msgstr "" - -#: includes/models/model.llms.coupon.php:111, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:82 -msgid "Percentage Discount" -msgstr "" - -#: includes/models/model.llms.coupon.php:164 -msgctxt "Remaining coupon uses" -msgid "Unlimited" -msgstr "" - -#: includes/models/model.llms.coupon.php:251 -msgid "This coupon has reached its usage limit and can no longer be used." -msgstr "" - -#: includes/models/model.llms.coupon.php:255 -msgid "This coupon expired on %s and can no longer be used." -msgstr "" - -#: includes/models/model.llms.coupon.php:259, -#: includes/models/model.llms.coupon.php:263 -msgid "This coupon cannot be used to purchase \"%s\"." -msgstr "" - -#: includes/models/model.llms.order.php:165 -msgctxt "default order note author" -msgid "LifterLMS" -msgstr "" - -#: includes/models/model.llms.order.php:414, -#: templates/admin/post-types/product-access-plan.php:176 -msgid "Lifetime Access" -msgstr "" - -#: includes/models/model.llms.order.php:426 -msgid "To be Determined" -msgstr "" - -#: includes/models/model.llms.order.php:539 -msgid "Anonymous" -msgstr "" - -#: includes/models/model.llms.order.php:555 -msgid "Order – %s" -msgstr "" - -#: includes/models/model.llms.order.php:555 -msgctxt "Order date parsed by strftime" -msgid "%b %d, %Y @ %I:%M %p" -msgstr "" - -#: includes/models/model.llms.order.php:583, -#: includes/models/model.llms.transaction.php:121 -msgid "Payment gateway %s could not be located or is no longer enabled" -msgstr "" - -#: includes/models/model.llms.order.php:687 -msgid "Order is not recurring" -msgstr "" - -#: includes/models/model.llms.order.php:689 -msgid "Invalid order status" -msgstr "" - -#: includes/models/model.llms.order.php:699 -msgid "No more payments due" -msgstr "" - -#: includes/models/model.llms.order.php:1264 -msgid "Order payment plan completed." -msgstr "" - -#: includes/models/model.llms.order.php:1311 -msgid "Automatic retry attempt scheduled for %s" -msgstr "" - -#: includes/models/model.llms.order.php:1322 -msgid "Maximum retry attempts reached." -msgstr "" - -#: includes/models/model.llms.quiz.attempt.php:491 -msgid "Quiz Attempt #%1$d by %2$s" -msgstr "" - -#: includes/models/model.llms.quiz.attempt.php:509 -msgid "You must be logged in to take a quiz!" -msgstr "" - -#: includes/models/model.llms.quiz.attempt.question.php:211 -msgid "Correct answer" -msgstr "" - -#: includes/models/model.llms.quiz.attempt.question.php:214 -msgid "Incorrect answer" -msgstr "" - -#: includes/models/model.llms.quiz.attempt.question.php:219 -msgid "Awaiting review" -msgstr "" - -#: includes/models/model.llms.student.php:656 -msgctxt "course grade when no quizzes taken or in course" -msgid "N/A" -msgstr "" - -#: includes/models/model.llms.student.php:677 -msgctxt "lesson grade when lesson has no quiz" -msgid "N/A" -msgstr "" - -#: includes/models/model.llms.student.php:794 -msgctxt "overall grade when no quizzes" -msgid "N/A" -msgstr "" - -#: includes/models/model.llms.transaction.php:71 -msgid "Transaction for Order #%1$d – %2$s" -msgstr "" - -#: includes/models/model.llms.transaction.php:71 -msgctxt "Transaction date parsed by strftime" -msgid "%1$b %2$d, %Y @ %I:%M %p" -msgstr "" - -#: includes/models/model.llms.transaction.php:184 -msgid "The selected transaction is not eligible for a refund." -msgstr "" - -#: includes/models/model.llms.transaction.php:192 -msgid "" -"Requested refund amount was %1$s, the maximum possible refund for this " -"transaction is %2$s." -msgstr "" - -#: includes/models/model.llms.transaction.php:201 -msgid "manual refund" -msgstr "" - -#: includes/models/model.llms.transaction.php:209 -msgid "Selected gateway \"%s\" is inactive or invalid." -msgstr "" - -#: includes/models/model.llms.transaction.php:212 -msgid "Selected gateway \"%s\" does not support refunds." -msgstr "" - -#: includes/models/model.llms.transaction.php:243 -msgid "Refunded %1$s for transaction #%2$d via %3$s [Refund ID: %4$s]" -msgstr "" - -#: includes/models/model.llms.transaction.php:247 -msgid "Refund Notes: " -msgstr "" - -#: includes/models/model.llms.transaction.php:277 -msgid "An unknown error occurred during refund processing" -msgstr "" - -#: includes/models/model.llms.transaction.php:298 -msgid "Single" -msgstr "" - -#: includes/models/model.llms.transaction.php:300, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:141 -msgid "Recurring" -msgstr "" - -#: includes/models/model.llms.transaction.php:302, -#: templates/checkout/form-summary.php:14 -msgid "Trial" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:79, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:163 -msgid "[Deleted]" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:91 -msgid "%1$s earned the achievement \"%2$s\"" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:97 -msgid "%1$s earned the certificate \"%2$s\"" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:103 -msgid "Email \"%1$s\" was sent to %2$s" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:109 -msgid "%1$s purchased the %2$s" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:116 -msgid "%1$s enrolled into the %2$s" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:118 -msgid "%1$s unenrolled from the %2$s" -msgstr "" - -#: includes/models/model.llms.user.postmeta.php:125 -msgid "%1$s completed the %2$s" -msgstr "" - -#. translators: %d = number of notifications -#: includes/privacy/class-llms-privacy-erasers.php:128 -msgid "Removed %d notifications." -msgstr "" - -#: includes/privacy/class-llms-privacy-erasers.php:148 -msgid "Order cancelled during personal data erasure." -msgstr "" - -#: includes/privacy/class-llms-privacy-erasers.php:160 -msgid "Personal data removed during personal data erasure." -msgstr "" - -#. translators: %s Prop name. -#: includes/privacy/class-llms-privacy-erasers.php:190 -msgid "Removed student \"%s\"" -msgstr "" - -#. translators: %d Order number. -#: includes/privacy/class-llms-privacy-erasers.php:248 -msgid "Removed personal data from order #%d." -msgstr "" - -#. translators: %d Order number. -#: includes/privacy/class-llms-privacy-erasers.php:254 -msgid "Personal data within order #%d has been retained." -msgstr "" - -#: includes/privacy/class-llms-privacy-erasers.php:294 -msgid "Removed all student course and membership enrollment and activity data." -msgstr "" - -#: includes/privacy/class-llms-privacy-erasers.php:299 -msgid "" -"Retained all student course and membership enrollment and activity data." -msgstr "" - -#. translators: %d quiz attempt id. -#: includes/privacy/class-llms-privacy-erasers.php:333 -msgid "Quiz attempt #%d removed." -msgstr "" - -#. translators: %d quiz attempt id. -#: includes/privacy/class-llms-privacy-erasers.php:341 -msgid "Quiz attempt #%d retained." -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:112, -#: includes/admin/reporting/tables/llms.table.achievements.php:154, -#: includes/admin/views/builder/question.php:78 -msgid "Image" -msgstr "" - -#. translators: %s = post type singular name label (Course or Membership) -#: includes/privacy/class-llms-privacy-exporters.php:169 -msgid "%s Title" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:174 -msgid "Enrollment Status" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:179 -msgid "Enrollment Date" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:186, -#: templates/admin/reporting/tabs/students/courses-course.php:59 -msgid "Last Activity" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:195, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:364, -#: includes/admin/reporting/tables/llms.table.course.students.php:408, -#: includes/admin/reporting/tables/llms.table.student.courses.php:209, -#: includes/admin/reporting/tables/llms.table.students.php:424, -#: templates/admin/reporting/tabs/students/courses-course.php:33 -msgid "Progress" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:204, -#: includes/privacy/class-llms-privacy-exporters.php:292, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:80, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:80, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:368, -#: includes/admin/reporting/tables/llms.table.course.students.php:413, -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:263, -#: includes/admin/reporting/tables/llms.table.student.course.php:191, -#: includes/admin/reporting/tables/llms.table.student.courses.php:206, -#: includes/admin/reporting/tables/llms.table.students.php:429, -#: templates/admin/reporting/tabs/quizzes/attempt.php:34 -msgid "Grade" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:276 -msgid "Attempt ID" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:281 -msgid "Attempt Number" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:512 -msgid "Personal Information" -msgstr "" - -#: includes/privacy/class-llms-privacy-exporters.php:542, -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:164 -msgid "Quiz Attempts" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:30, -#: includes/privacy/class-llms-privacy.php:41 -msgid "Student Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:31 -msgid "Course Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:32, -#: includes/privacy/class-llms-privacy.php:42 -msgid "Quiz Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:33 -msgid "Membership Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:34, -#: includes/privacy/class-llms-privacy.php:43, -#: includes/privacy/class-llms-privacy.php:45 -msgid "Order Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:35, -#: includes/privacy/class-llms-privacy.php:44 -msgid "Achievement Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:36 -msgid "Certificate Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:46 -msgid "Notification Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:48 -msgid "Postmeta Data" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:87 -msgid "Order Number" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:88, -#: includes/admin/views/metaboxes/view-order-submit.php:31 -msgid "Order Date" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:89, -#: templates/myaccount/view-order.php:47, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:44 -msgid "Product" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:90, -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:80, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:80, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:41 -msgid "Plan" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:94 -msgid "User ID" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:99 -msgid "Billing First Name" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:100 -msgid "Billing Last Name" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:101 -msgid "Billing Email" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:109, -#: includes/privacy/class-llms-privacy.php:208 -msgid "IP Address" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:127 -msgid "" -"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." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:130 -msgid "" -"We collect information about you during the registration, enrollment, and " -"checkout processes on our site." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:131 -msgid "What we collect and store" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:132 -msgid "" -"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:" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:134 -msgid "" -"Send you information about your account, orders, courses, and memberships" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:135 -msgid "" -"Communicate with you about courses and memberships that you’re enrolled in" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:136 -msgid "Respond to your requests, including refunds and complaints" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:137 -msgid "Process payments and prevent fraud" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:138 -msgid "Set up your account for our site" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:139 -msgid "Comply with any legal obligations we have" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:140 -msgid "Improve our site’s offerings" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:141 -msgid "Send you marketing messages, if you choose to receive them" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:143 -msgid "" -"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." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:144 -msgid "" -"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." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:145 -msgid "We will also store comments or reviews, if you chose to leave them." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:146 -msgid "Who on our team has access" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:147 -msgid "" -"Members of our team have access to the information you provide us. For " -"example, both Administrators and Site Managers can access:" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:149 -msgid "" -"Order information like what was purchased, when it was purchased and where " -"it should be sent, and" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:150 -msgid "" -"Customer information like your name, email address, and billing information." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:152 -msgid "" -"Course and membership instructors can access your course progress and " -"activities including:" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:154 -msgid "Enrollment dates for their courses and memberships" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:155 -msgid "Course progress and status information for their courses" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:156 -msgid "Quiz and assignments answers and grades for their courses" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:157 -msgid "Comments and reviews made on their memberships and courses" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:159 -msgid "" -"Our team members have access to this information to help fulfill orders, " -"process refunds, and support you." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:160 -msgid "What we share with others" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:162 -msgid "" -"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." -msgstr "" - -#: includes/privacy/class-llms-privacy.php:164 -msgid "" -"We share information with third parties who help us provide our orders and " -"store services to you; for example --" -msgstr "" - -#: includes/privacy/class-llms-privacy.php:209 -msgid "Last Login Date" -msgstr "" - -#: includes/processors/class.llms.processor.table.to.csv.php:210 -msgid "Your %1$s export file from %2$s" -msgstr "" - -#: includes/processors/class.llms.processor.table.to.csv.php:211 -msgid "Please find the attached CSV file." -msgstr "" - -#: includes/shortcodes/class.llms.shortcode.checkout.php:44 -msgid "" -"You are currently logged in as <em>%1$s</em>. <a href=\"%2$s\">Click here to " -"logout</a>" -msgstr "" - -#: includes/shortcodes/class.llms.shortcode.checkout.php:46 -msgid "Already have an account? <a href=\"%s\">Click here to login</a>" -msgstr "" - -#: includes/shortcodes/class.llms.shortcode.checkout.php:162 -msgid "Invalid access plan." -msgstr "" - -#: includes/shortcodes/class.llms.shortcode.checkout.php:191 -msgid "" -"Your cart is currently empty. Click <a href=\"%s\">here</a> to get started." -msgstr "" - -#: includes/shortcodes/class.llms.shortcode.courses.php:164 -msgid "" -"You must be logged in to view this information. Click %1$shere%2$s to login." -msgstr "" - -#: includes/shortcodes/class.llms.shortcodes.php:289, -#: templates/loop/pagination.php:23 -msgid "Previous" -msgstr "" - -#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:22 -msgid "Displays a list of bbPress forums associated with the course." -msgstr "" - -#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:25 -msgid "LifterLMS Course Forums List" -msgstr "" - -#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:75 -msgid "Course Forums" -msgstr "" - -#: includes/widgets/class.llms.widget.course.progress.php:18 -msgid "Course Progress" -msgstr "" - -#: includes/widgets/class.llms.widget.course.progress.php:20 -msgid "Displays Course Progress on Course or Lesson" -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:18 -msgid "Course Syllabus" -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:20 -msgid "Displays All Course lessons on Course or Lesson page" -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:45 -msgid "Make outline collapsible?" -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:46 -msgid "" -"Allow students to hide lessons within a section by clicking the section " -"titles." -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:53 -msgid "Display open and close all toggles" -msgstr "" - -#: includes/widgets/class.llms.widget.course.syllabus.php:54 -msgid "" -"Display \"Open All\" and \"Close All\" toggles at the bottom of the outline." -msgstr "" - -#: templates/achievements/loop.php:30 -msgid "" -"You do not have any achievements yet. Enroll in a course to get started!" -msgstr "" - -#: templates/achievements/template.php:22 -msgctxt "achievement earned date" -msgid "Awarded on %s" -msgstr "" - -#: templates/admin/user-edit.php:22 -msgid "required" -msgstr "" - -#: templates/certificates/loop.php:30 -msgid "You do not have any certificates yet." -msgstr "" - -#: templates/checkout/form-checkout.php:26, -#: templates/checkout/form-confirm-payment.php:19 -msgid "Billing Information" -msgstr "" - -#: templates/checkout/form-checkout.php:28, -#: templates/admin/reporting/tabs/students/information.php:18 -msgid "Student Information" -msgstr "" - -#: templates/checkout/form-checkout.php:50, -#: templates/checkout/form-confirm-payment.php:37 -msgid "Order Summary" -msgstr "" - -#: templates/checkout/form-checkout.php:74, -#: templates/checkout/form-confirm-payment.php:53 -msgid "Payment Details" -msgstr "" - -#: templates/checkout/form-checkout.php:76 -msgid "Enrollment Confirmation" -msgstr "" - -#: templates/checkout/form-checkout.php:107 -msgid "Buy Now" -msgstr "" - -#: templates/checkout/form-checkout.php:107 -msgid "Enroll Now" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:58, -#: templates/checkout/form-switch-source.php:59 -msgid "Payment Method:" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:74, -#: includes/admin/settings/class.llms.settings.checkout.php:184 -msgid "Confirm Payment" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:131 -msgid "The order for this transaction could not be located." -msgstr "" - -#: templates/checkout/form-confirm-payment.php:144, -#: templates/checkout/form-confirm-payment.php:187 -msgid "Confirm Purchase" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:159 -msgid "Payment Terms:" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:164 -msgid "Price:" -msgstr "" - -#: templates/checkout/form-confirm-payment.php:177, -#: templates/myaccount/view-order.php:121, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:43 -msgid "Payment Method" -msgstr "" - -#: templates/checkout/form-coupon.php:19 -msgid "Have a coupon?" -msgstr "" - -#: templates/checkout/form-coupon.php:20 -msgid "Click here to enter your code" -msgstr "" - -#: templates/checkout/form-coupon.php:30 -msgid "Coupon Code" -msgstr "" - -#: templates/checkout/form-coupon.php:41 -msgid "Apply Coupon" -msgstr "" - -#: templates/checkout/form-coupon.php:51 -msgid "Coupon code \"%s\" has been applied to your order." -msgstr "" - -#: templates/checkout/form-coupon.php:59 -msgid "Remove Coupon" -msgstr "" - -#: templates/checkout/form-gateways.php:28 -msgid "Payment processing is currently disabled." -msgstr "" - -#: templates/checkout/form-gateways.php:62 -msgid "" -"There are no gateways enabled which can support the necessary transaction " -"type for this access plan." -msgstr "" - -#: templates/checkout/form-summary.php:39 -msgid "Access" -msgstr "" - -#: templates/checkout/form-switch-source.php:17 -msgid "Save Payment Method" -msgstr "" - -#: templates/checkout/form-switch-source.php:19, -#: templates/checkout/form-switch-source.php:31 -msgid "Reactivate Subscription" -msgstr "" - -#: templates/checkout/form-switch-source.php:21 -msgid "Save and Pay Now" -msgstr "" - -#: templates/checkout/form-switch-source.php:31, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:97, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:110 -msgid "Update Payment Method" -msgstr "" - -#: templates/checkout/form-switch-source.php:50 -msgid "Due Now: %s" -msgstr "" - -#: templates/course/author.php:18 -msgid "Course Instructor" -msgid_plural "Course Instructors" -msgstr[0] "" -msgstr[1] "" - -#: templates/course/categories.php:14 -msgid "Categories: " -msgstr "" - -#: templates/course/complete-lesson-link.php:42, -#: includes/notifications/controllers/class.llms.notification.controller.lesson.complete.php:90 -msgid "Lesson Complete" -msgstr "" - -#: templates/course/complete-lesson-link.php:59 -msgid "Mark Incomplete" -msgstr "" - -#: templates/course/complete-lesson-link.php:90 -msgid "Mark Complete" -msgstr "" - -#: templates/course/complete-lesson-link.php:110 -msgid "Take Quiz" -msgstr "" - -#: templates/course/difficulty.php:20 -msgid "Difficulty: <span class=\"difficulty\">%s</span>" -msgstr "" - -#: templates/course/length.php:18 -msgid "Estimated Time: <span class=\"length\">%s</span>" -msgstr "" - -#: templates/course/lesson-navigation.php:24, -#: templates/course/lesson-navigation.php:68, -#: templates/course/lesson-navigation.php:69 -msgid "Previous Lesson" -msgstr "" - -#: templates/course/lesson-navigation.php:35, -#: templates/course/lesson-navigation.php:84, -#: templates/course/lesson-navigation.php:85 -msgid "Back to Course" -msgstr "" - -#: templates/course/lesson-navigation.php:48, -#: templates/course/lesson-navigation.php:97, -#: templates/course/lesson-navigation.php:98, -#: templates/quiz/start-button.php:41 -msgid "Next Lesson" -msgstr "" - -#: templates/course/lesson-preview.php:29 -msgctxt "lesson order within section" -msgid "%1$d of %2$d" -msgstr "" - -#: templates/course/meta-wrapper-start.php:12 -msgctxt "course meta info title" -msgid "Course Information" -msgstr "" - -#: templates/course/outline-list-small.php:95 -msgid "Open All" -msgstr "" - -#: templates/course/outline-list-small.php:97 -msgid "Close All" -msgstr "" - -#: templates/course/parent-course.php:14 -msgid "" -"<p class=\"llms-parent-course-link\">Back to: <a class=\"llms-lesson-link\" " -"href=\"%1$s\">%2$s</a></p>" -msgstr "" - -#: templates/course/prerequisites.php:19 -msgid "" -"Before starting this course you must complete the required prerequisite " -"course: %s" -msgstr "" - -#: templates/course/prerequisites.php:26 -msgid "" -"Before starting this course you must complete the required prerequisite " -"track: %s" -msgstr "" - -#: templates/course/syllabus.php:27 -msgid "This course does not have any sections." -msgstr "" - -#: templates/course/syllabus.php:52 -msgid "This section does not have any lessons." -msgstr "" - -#: templates/course/tags.php:14 -msgid "Tags: " -msgstr "" - -#: templates/course/tracks.php:14 -msgid "Tracks: " -msgstr "" - -#: templates/emails/reset-password.php:10 -msgid "Someone recently requested that the password be reset for %s." -msgstr "" - -#: templates/emails/reset-password.php:12 -msgid "To reset your password, click on the button below:" -msgstr "" - -#: templates/emails/reset-password.php:16 -msgid "" -"If this was a mistake you can ignore this email and your password will not " -"be changed." -msgstr "" - -#: templates/emails/reset-password.php:20 -msgid "Trouble clicking? Copy and paste this URL into your browser:" -msgstr "" - -#: templates/global/form-registration.php:23, -#: templates/global/form-registration.php:58 -msgid "Register" -msgstr "" - -#: templates/loop/enroll-date.php:18 -msgid "Enrolled: %s" -msgstr "" - -#: templates/loop/enroll-status.php:18 -msgid "Status: %s" -msgstr "" - -#: templates/loop/none-found.php:4 -msgid "No products were found matching your selection." -msgstr "" - -#: templates/loop/view-link.php:35 -msgid "Learn More" -msgstr "" - -#: templates/membership/price.php:25 -msgid "or" -msgstr "" - -#: templates/myaccount/form-redeem-voucher.php:22 -msgctxt "Voucher Code" -msgid "Submit" -msgstr "" - -#: templates/myaccount/my-notifications.php:24 -msgid "You have no notifications." -msgstr "" - -#: templates/myaccount/my-orders.php:13 -msgid "No orders found." -msgstr "" - -#: templates/myaccount/my-orders.php:21, templates/myaccount/my-orders.php:34 -msgid "Expires" -msgstr "" - -#: templates/myaccount/my-orders.php:22, templates/myaccount/my-orders.php:41 -msgid "Next Payment" -msgstr "" - -#: templates/myaccount/view-order-transactions.php:19, -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:81, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:81, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:42, -#: templates/admin/post-types/order-transactions.php:20 -msgid "Amount" -msgstr "" - -#: templates/myaccount/view-order-transactions.php:20, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:125 -msgid "Method" -msgstr "" - -#: templates/myaccount/view-order.php:22 -msgid "Invalid Order" -msgstr "" - -#: templates/myaccount/view-order.php:26, -#: includes/admin/post-types/tables/class.llms.table.student.management.php:159 -msgid "Order #%d" -msgstr "" - -#: templates/myaccount/view-order.php:54, templates/myaccount/view-order.php:79 -msgid "Original Total" -msgstr "" - -#: templates/myaccount/view-order.php:59, templates/myaccount/view-order.php:95 -msgid "Coupon Discount" -msgstr "" - -#: templates/myaccount/view-order.php:69 -msgid "Trial Total" -msgstr "" - -#: templates/myaccount/view-order.php:72, -#: templates/myaccount/view-order.php:112, -#: templates/admin/post-types/order-details.php:109, -#: templates/admin/post-types/order-details.php:146 -msgid "for %1$d %2$s" -msgid_plural "for %1$d %2$ss" -msgstr[0] "" -msgstr[1] "" - -#: templates/myaccount/view-order.php:85 -msgid "Sale Discount" -msgstr "" - -#: templates/myaccount/view-order.php:106 -msgid "Total" -msgstr "" - -#: templates/myaccount/view-order.php:110, -#: templates/admin/post-types/order-details.php:144 -msgid "Every %2$s" -msgid_plural "Every %1$d %2$ss" -msgstr[0] "" -msgstr[1] "" - -#: templates/myaccount/view-order.php:115, -#: templates/admin/post-types/order-details.php:149, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:143 -msgid "One-time" -msgstr "" - -#: templates/myaccount/view-order.php:133, -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:268, -#: templates/admin/reporting/tabs/quizzes/attempt.php:85 -msgid "Start Date" -msgstr "" - -#: templates/myaccount/view-order.php:138 -msgid "Last Payment Date" -msgstr "" - -#: templates/myaccount/view-order.php:144, -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:134, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:134, -#: includes/admin/views/metaboxes/view-order-submit.php:57 -msgid "Next Payment Date" -msgstr "" - -#: templates/myaccount/view-order.php:158, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:37 -msgid "Expiration Date" -msgstr "" - -#: templates/myaccount/view-order.php:189 -msgid "Cancel Subscription" -msgstr "" - -#: templates/product/pricing-table.php:32 -msgid "FEATURED" -msgstr "" - -#: templates/product/pricing-table.php:47 -msgid "SALE" -msgstr "" - -#: templates/product/pricing-table.php:68 -msgid "sale ends %s" -msgstr "" - -#: templates/product/pricing-table.php:75 -msgid "MEMBER PRICING" -msgstr "" - -#: templates/product/pricing-table.php:93 -msgid "TRIAL" -msgstr "" - -#: templates/quiz/meta-information.php:17 -msgid "Quiz Information" -msgstr "" - -#: templates/quiz/meta-information.php:21 -msgid "Minimum Passing Grade: %s" -msgstr "" - -#: templates/quiz/meta-information.php:26 -msgid "Remaining Attempts: %s" -msgstr "" - -#: templates/quiz/meta-information.php:30 -msgid "Questions: %s" -msgstr "" - -#: templates/quiz/meta-information.php:35 -msgid "Time Limit: %s" -msgstr "" - -#: templates/quiz/results-attempt-questions-list.php:34 -msgid "%1$d / %2$d points" -msgstr "" - -#: templates/quiz/results-attempt-questions-list.php:53 -msgid "Selected answer: " -msgstr "" - -#: templates/quiz/results-attempt-questions-list.php:62 -msgid "Correct answer: " -msgstr "" - -#: templates/quiz/results-attempt-questions-list.php:70 -msgid "Clarification: " -msgstr "" - -#: templates/quiz/results-attempt-questions-list.php:79 -msgid "Instructor remarks: " -msgstr "" - -#: templates/quiz/results-attempt.php:20 -msgid "Attempt #%d Results" -msgstr "" - -#: templates/quiz/results-attempt.php:25 -msgid "Correct Answers: %1$d / %2$d" -msgstr "" - -#: templates/quiz/results-attempt.php:26, -#: templates/admin/reporting/tabs/students/courses-course.php:72 -msgid "Completed: %s" -msgstr "" - -#: templates/quiz/results-attempt.php:27 -msgid "Total time: %s" -msgstr "" - -#: templates/quiz/results.php:46 -msgid "View Previous Attempts" -msgstr "" - -#: templates/quiz/results.php:48 -msgid "Select an Attempt" -msgstr "" - -#. translators: 1: attempt number; 2: grade percentage; 3: pass/fail text -#: templates/quiz/results.php:52 -msgid "Attempt #%1$d - %2$s (%3$s)" -msgstr "" - -#: templates/quiz/return-to-lesson.php:19 -msgid "Return to Lesson" -msgstr "" - -#: templates/quiz/start-button.php:33 -msgid "Start Quiz" -msgstr "" - -#: templates/quiz/start-button.php:37, templates/quiz/start-button.php:47 -msgid "You are not able take this quiz" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:39 -msgid "Course Analytics" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:95, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:63 -msgid "Select a Course" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:99, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:38 -msgid "All Courses" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:118, -#: includes/admin/analytics/class.llms.analytics.memberships.php:115, -#: includes/admin/analytics/class.llms.analytics.sales.php:132 -msgid "Filter Date Range" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:134, -#: includes/admin/analytics/class.llms.analytics.memberships.php:131, -#: includes/admin/analytics/class.llms.analytics.sales.php:148 -msgid "Start date" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:140, -#: includes/admin/analytics/class.llms.analytics.memberships.php:137, -#: includes/admin/analytics/class.llms.analytics.sales.php:154 -msgid "End date" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:180, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:22 -msgid "Student Enrollment" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:204, -#: includes/admin/analytics/class.llms.analytics.memberships.php:201 -msgid "Lesson Completion Percentage" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:222 -msgid "Enrolled Students" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:240 -msgid "All Students" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:253 -msgid "Current Students" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:266 -msgid "Completion %" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.courses.php:279 -msgid "Certificates Issued" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:36 -msgid "Membership Analytics" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:93, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:86 -msgid "Select a Membership" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:97 -msgid "All Memberships" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:183 -msgid "Membership Enrollment by Day" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:219 -msgid "Members" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:237 -msgid "All Members" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:250 -msgid "Current Members" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:263 -msgid "Retention %" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.memberships.php:277 -msgid "Expired Members" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:19, -#: includes/admin/reporting/class.llms.admin.reporting.php:252 -msgid "Sales" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:37 -msgid "Sales Analytics" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:93 -msgid "Select a product" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:97 -msgid "All Products" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:206 -msgid "Sales Volume" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:222 -msgid "Total Sold" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:235 -msgid "Total Sales" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:248 -msgid "Coupons Used" -msgstr "" - -#: includes/admin/analytics/class.llms.analytics.sales.php:261 -msgid "Total Coupons" -msgstr "" - -#: includes/admin/post-types/class.llms.meta.boxes.php:144 -msgid "Export CSV" -msgstr "" - -#: includes/admin/post-types/class.llms.post.tables.php:77 -msgid "Missing post ID." -msgstr "" - -#: includes/admin/post-types/class.llms.post.tables.php:83 -msgid "Invalid post ID." -msgstr "" - -#: includes/admin/post-types/class.llms.post.tables.php:87 -msgid "Action cannot be executed on the current post." -msgstr "" - -#: includes/admin/post-types/class.llms.post.tables.php:127, -#: includes/admin/post-types/class.llms.post.tables.php:129 -msgid "Filter by %s" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:194 -msgid "Today" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:195 -msgid "Yesterday" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:196 -msgid "This Week" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:197 -msgid "Last Week" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:198, -#: templates/admin/analytics/analytics.php:41, -#: templates/admin/reporting/nav-filters.php:23 -msgid "This Month" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:199, -#: templates/admin/analytics/analytics.php:37, -#: templates/admin/reporting/nav-filters.php:19 -msgid "Last Month" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:200, -#: templates/admin/analytics/analytics.php:33, -#: templates/admin/reporting/nav-filters.php:15 -msgid "This Year" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:201 -msgid "Last Year" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:202 -msgid "All Time" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:249, -#: templates/admin/analytics/analytics.php:71, -#: templates/admin/reporting/nav-filters.php:53, -#: includes/admin/reporting/tables/llms.table.course.students.php:252, -#: includes/admin/reporting/tables/llms.table.courses.php:286, -#: includes/admin/reporting/tables/llms.table.students.php:259, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:91, -#: templates/admin/reporting/tabs/students/student.php:13 -msgid "Students" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:253, -#: includes/admin/settings/class.llms.settings.general.php:174, -#: includes/admin/reporting/tables/llms.table.students.php:433, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:60 -msgid "Enrollments" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:331 -msgid "You don't have permission to do that" -msgstr "" - -#: includes/admin/reporting/class.llms.admin.reporting.php:434 -msgid "Previously %s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:26 -msgid "Accounts" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:42 -msgid "Required" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:43 -msgid "Optional" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:62 -msgid "Dashboard Page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:76 -msgid "Courses Sorting" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:78 -msgid "" -"Determines the order of the courses in-progress listed on the student " -"dashboard." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:82 -msgid "Course Title (A to Z)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:83 -msgid "Course Title (Z to A)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:84 -msgid "Enrollment Date (Most Recent to Least Recent)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:85, -#: includes/admin/settings/class.llms.settings.courses.php:101, -#: includes/admin/settings/class.llms.settings.memberships.php:110 -msgid "Order (Low to High)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:86 -msgid "Order (High to Low)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:103 -msgid "Student Dashboard Endpoints" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:104 -msgid "" -"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." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:109 -msgid "View Courses" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:110 -msgid "List of all the student's courses" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:118 -msgid "View Memberships" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:119 -msgid "List of all the student's memberships" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:127 -msgid "View Achievements" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:128 -msgid "List of all the student's achievements" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:136 -msgid "View Certificates" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:137 -msgid "List of all the student's certificates" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:146 -msgid "View Notifications and adjust notification settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:155 -msgid "Edit Account page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:163 -msgid "Lost Password" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:164 -msgid "Lost Password page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:172 -msgid "Redeem Vouchers" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:173 -msgid "Redeem vouchers page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:181 -msgid "Orders History" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:182 -msgid "Students can review order history on this page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:200 -msgid "User Information Options" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:206 -msgid "These settings apply to all user information screens." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:207 -msgid "General Information Field Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:211 -msgid "Disable Usernames" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:212 -msgid "" -"Automatically generate student usernames and enable email address login." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:218 -msgid "Password Strength" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:219 -msgid "Add a password strength meter" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:226 -msgid "" -"Select the minimum password strength allowed when students create a new " -"password." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:231 -msgctxt "password strength meter" -msgid "Weak" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:232 -msgctxt "password strength meter" -msgid "Medium" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:233 -msgctxt "password strength meter" -msgid "Strong" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:237 -msgid "Terms and Conditions" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:244 -msgid "" -"When enabled users must agree to your site's Terms and Conditions to " -"register for an account." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:254 -msgid "Select a page where your site's Terms and Conditions are described." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:259 -msgid "Terms and Conditions Page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:263, -#: includes/admin/settings/class.llms.settings.accounts.php:294, -#: includes/admin/settings/class.llms.settings.courses.php:79, -#: includes/admin/settings/class.llms.settings.memberships.php:86, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:95, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:123 -msgid "Select a page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:271 -msgid "" -"Customize the text used to display the Terms and Conditions checkbox that " -"students must accept." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:272 -msgid "Terms and Conditions Notice" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:278 -msgid "Privacy Policy" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:284 -msgid "" -"Select a page where your site's Privacy Policy is described. See " -"%1$sWordPress Privacy Settings%2$s for more information" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:290 -msgid "Privacy Policy Page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:302 -msgid "" -"Optionally display a privacy policy notice during registration and checkout." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:303 -msgid "Privacy Policy Notice" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:308 -msgid "Account Erasure Requests" -msgstr "" - -#. translators: %$1s = opening anchor to account erasure screen; %2$s closing anchor -#: includes/admin/settings/class.llms.settings.accounts.php:310 -msgid "Customize data retention during %1$saccount erasure requests%2$s." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:317 -msgid "When enabled orders will be anonymized during a personal data erasure." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:318 -msgid "Remove Order Data" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:325 -msgid "" -"When enabled all student data related to course and membership activities " -"will be removed." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:326 -msgid "Remove Student LMS Data" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:331 -msgid "Customize the user information fields available on the checkout screen." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:332 -msgid "Checkout Fields" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:339, -#: includes/admin/settings/class.llms.settings.accounts.php:384, -#: includes/admin/settings/class.llms.settings.accounts.php:433 -msgid "First & Last Name" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:347, -#: includes/admin/settings/class.llms.settings.accounts.php:392, -#: includes/admin/settings/class.llms.settings.accounts.php:441 -msgid "Address" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:362, -#: includes/admin/settings/class.llms.settings.accounts.php:407, -#: includes/admin/settings/class.llms.settings.accounts.php:456 -msgid "Add an email confirmation field" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:364, -#: includes/admin/settings/class.llms.settings.accounts.php:409, -#: includes/admin/settings/class.llms.settings.accounts.php:458 -msgid "Email Confirmation" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:369 -msgid "" -"Customize the user information fields available on the open registration " -"screen." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:370 -msgid "Open Registration Fields" -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:375 -msgid "" -"Allow registration on the Account Access Page without enrolling in a course " -"or membership." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:415 -msgid "If required, users can only use open registration with a voucher." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:416 -msgid "If optional, users may redeem a voucher during open registration." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:417 -msgid "If hidden, users can only redeem vouchers on their dashboard." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:425 -msgid "" -"Customize the user information fields available on the account update screen." -msgstr "" - -#: includes/admin/settings/class.llms.settings.accounts.php:426 -msgid "Account Update Fields" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:58, -#: templates/admin/post-types/order-transactions.php:23 -msgid "Gateway" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:59 -msgid "Gateway ID" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:60, -#: includes/admin/settings/class.llms.settings.checkout.php:74, -#: includes/admin/settings/class.llms.settings.checkout.php:75, -#: includes/admin/settings/class.llms.settings.general.php:93, -#: includes/admin/settings/class.llms.settings.integrations.php:123, -#: includes/admin/settings/class.llms.settings.integrations.php:147, -#: includes/admin/settings/class.llms.settings.integrations.php:148 -msgid "Enabled" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:111, -#: includes/admin/settings/class.llms.settings.checkout.php:166 -msgid "Checkout Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:171 -msgid "Checkout Page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:172 -msgid "Page used for displaying the checkout form." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:185 -msgid "Payment confirmation endpoint slug" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:194 -msgid "Force SSL" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:195 -msgid "Force secure checkout via SSL (https) on the checkout page(s)." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:196 -msgid "Requires an SSL certificate. %1$sLearn More%2$s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:204 -msgid "Enable automatic retry of failed recurring payments." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:205 -msgid "" -"Recover lost revenue from temporarily declined payment methods. %1$sLearn " -"more%2$s." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:207 -msgid "Retry Failed Payments" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:222 -msgid "Currency Options" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:224 -msgid "The following options affect how prices are displayed on the frontend." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:231 -msgid "" -"Select the country LifterLMS should use as the default during transactions " -"and registrations." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:241 -msgid "Currency" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:242 -msgid "" -"Select the currency LifterLMS should use to display prices and process " -"transactions." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:251 -msgid "Currency Position" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:252 -msgid "" -"Customize the position and formatting of the currency symbol for displayed " -"prices." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:265 -msgid "Thousand Separator" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:267 -msgid "" -"Choose the character to display as the thousand's place separator for " -"displayed prices." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:274 -msgid "Decimal Separator" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:276 -msgid "" -"Choose the character to display as the decimal separator for displayed " -"prices." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:283 -msgid "Decimal Places" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:285 -msgid "Customize the number of decimal places for prices." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:292 -msgid "Hide Zero Decimals" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:293 -msgid "Automatically remove zero decimals from the end of displayed prices." -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:351 -msgid "Payment Gateway Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:357 -msgid "Error: \"%s\" is not a valid payment gateway" -msgstr "" - -#: includes/admin/settings/class.llms.settings.checkout.php:365 -msgid "%s Payment Gateway Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:45 -msgid "Course Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:50 -msgid "" -"Enabling this setting allows students to mark a lesson as \"incomplete\" " -"after they have completed a lesson." -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:53 -msgid "Retake Lessons" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:70 -msgid "Course Catalog Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:81 -msgid "" -"This page is where your visitors will find a list of all your available " -"courses. %1$sMore information%2$s." -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:90 -msgid "To show all courses on one page, enter -1" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:92 -msgid "Courses per page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:98 -msgid "Determines the display order for courses on the courses page." -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:102, -#: includes/admin/settings/class.llms.settings.memberships.php:111 -msgid "Title (A - Z)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:103, -#: includes/admin/settings/class.llms.settings.memberships.php:112 -msgid "Title (Z - A)" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:104, -#: includes/admin/settings/class.llms.settings.memberships.php:113 -msgid "Most Recent" -msgstr "" - -#: includes/admin/settings/class.llms.settings.courses.php:106 -msgid "Catalog Sorting" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:44, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:21 -msgid "Email Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:46 -msgid "" -"Settings for all emails sent by LifterLMS. Notification and engagement " -"emails will adhere to these settings." -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:50 -msgid "Sender Name" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:51 -msgid "Name to be displayed in From field" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:58 -msgid "Sender Email" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:59 -msgid "Email Address displayed in the From field" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:66 -msgid "Header Image" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:73 -msgid "Email Footer Text" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:74 -msgid "Text you would like displayed in the footer of all emails." -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:93 -msgid "Certificates Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:101 -msgid "Background Image Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:102 -msgid "" -"Use these sizes to determine the dimensions of certificate background " -"images. After changing these settings, you may need to <a href=\"http://" -"wordpress.org/extend/plugins/regenerate-thumbnails/\" target=\"_blank" -"\">regenerate your thumbnails</a>." -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:107 -msgid "Image Width" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:108, -#: includes/admin/settings/class.llms.settings.engagements.php:118 -msgid "in pixels" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:116 -msgid "Image Height" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:125 -msgid "Legacy compatibility" -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:126 -msgid "Use legacy certificate image sizes." -msgstr "" - -#: includes/admin/settings/class.llms.settings.engagements.php:127 -msgid "" -"Enabling this will override the above dimension settings and set the image " -"dimensions to match the dimensions of the uploaded image." -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:20, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:118, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:58, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:44 -msgid "General" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:59 -msgid "Quick Links" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:64 -msgid "Version: %s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:65 -msgid "Need help? Get support on the %1$sforums%2$s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:66 -msgid "" -"Looking for a quickstart guide, shortcodes, or developer documentation? Get " -"started at %s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:67 -msgid "Get LifterLMS news, updates, and more on our %1$sblog%2$s" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:85 -msgid "Features" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:92 -msgid "Automatic Recurring Payments: <strong>%s</strong>" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:93 -msgid "Disabled" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:122 -msgid "Select user roles" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:125 -msgid "" -"Users with the selected roles will bypass enrollment, drip, and prerequisite " -"restrictions for courses and memberships." -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:128 -msgid "Unrestricted Preview Access" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:157 -msgid "Activity This Week" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:176, -#: includes/admin/settings/class.llms.settings.general.php:182, -#: includes/admin/settings/class.llms.settings.general.php:188, -#: includes/admin/settings/class.llms.settings.general.php:194, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:56, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:62, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:68, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:74, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:56, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:62, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:68, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:74, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:88, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:94 -msgid "loading..." -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:177, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:63 -msgid "Number of total enrollments during the selected period" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:180, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:54 -msgid "Registrations" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:183, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:57 -msgid "Number of total user registrations during the selected period" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:186, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:60, -#: includes/admin/reporting/widgets/class.llms.analytics.widget.sold.php:24 -msgid "Net Sales" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:189, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:63 -msgid "Total of all successful transactions during this period" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:192, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:72 -msgid "Lessons Completed" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:195, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:75 -msgid "Number of total lessons completed during the selected period" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:219 -msgid "Most Popular Add-ons, Services, and Resources" -msgstr "" - -#: includes/admin/settings/class.llms.settings.general.php:220 -msgid "View More →" -msgstr "" - -#: includes/admin/settings/class.llms.settings.integrations.php:20, -#: includes/admin/settings/class.llms.settings.integrations.php:43, -#: includes/admin/settings/class.llms.settings.integrations.php:78 -msgid "Integrations" -msgstr "" - -#: includes/admin/settings/class.llms.settings.integrations.php:120 -msgid "Integration" -msgstr "" - -#: includes/admin/settings/class.llms.settings.integrations.php:121 -msgid "Integration ID" -msgstr "" - -#: includes/admin/settings/class.llms.settings.integrations.php:122, -#: includes/admin/settings/class.llms.settings.integrations.php:137, -#: includes/admin/settings/class.llms.settings.integrations.php:138 -msgid "Installed" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:45, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:21 -msgid "Membership Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:54 -msgid "Select a membership" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:57 -msgid "" -"Only allow access to site to users with a specific membership level. Users " -"will be able to view and purchase membership level." -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:60 -msgid "Restrict site by membership level" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:77 -msgid "Memberships Catalog" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:92 -msgid "Memberships Page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:97 -msgid "Memberships per page" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:98 -msgid "To show all memberships on one page, enter -1" -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:107 -msgid "Determines the display order for items on the memberships page." -msgstr "" - -#: includes/admin/settings/class.llms.settings.memberships.php:115 -msgid "Memberships Sorting" -msgstr "" - -#: includes/admin/settings/class.llms.settings.notifications.php:36 -msgid "All Notifications" -msgstr "" - -#: includes/admin/settings/class.llms.settings.notifications.php:81 -msgid "Subscribers" -msgstr "" - -#: includes/admin/settings/class.llms.settings.notifications.php:121 -msgid "Notification Settings" -msgstr "" - -#: includes/admin/settings/class.llms.settings.notifications.php:139 -msgid "Invalid notification" -msgstr "" - -#: includes/admin/views/html.admin.settings.php:36 -msgid "Save Changes" -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:38, -#: includes/forms/controllers/class.llms.controller.account.php:45 -msgid "Something went wrong. Please try again." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:48 -msgid "Subscription cancelled by student from account page." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:54 -msgid "Enrollment will be cancelled at the end of the prepaid period." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:83 -msgid "Please log in and try again." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:101 -msgid "Your account information has been saved." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:126, -#: includes/forms/frontend/class.llms.frontend.password.php:26 -msgid "Enter a username or e-mail address." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:150 -msgid "Invalid username or e-mail address." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:160 -msgid "Password reset is not allowed for this user" -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:181 -msgid "Check your e-mail for the confirmation link." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:185 -msgid "Unable to reset password due to an unknown error. Please try again." -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:216, -#: includes/forms/controllers/class.llms.controller.account.php:223 -msgid "Invalid Key" -msgstr "" - -#: includes/forms/controllers/class.llms.controller.account.php:232 -msgid "Your password has been updated. %1$sClick here to login%2$s" -msgstr "" - -#: includes/forms/controllers/class.llms.controller.registration.php:67 -msgid "Already logged in! Please log out and try again." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.forms.php:64 -msgid "Please enter your password." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.forms.php:71 -msgid "Passwords do not match." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.forms.php:125 -msgid "Voucher redeemed successfully!" -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.password.php:34 -msgid "The email address entered is not associated with an account." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.password.php:51 -msgid "Invalid username or e-mail." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.password.php:65 -msgid "Could not reset password." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.password.php:97 -msgid "Check your e-mail for the account confirmation link." -msgstr "" - -#: includes/forms/frontend/class.llms.frontend.password.php:113, -#: includes/forms/frontend/class.llms.frontend.password.php:120, -#: includes/forms/frontend/class.llms.frontend.password.php:129 -msgid "Invalid key" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.achievement.earned.php:82 -msgid "Achievement Earned" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php:82 -msgid "Certificate Earned" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.course.track.complete.php:81 -msgid "Course Track Complete" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.enrollment.php:88 -msgid "Enrollment" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:108 -msgid "Gateway: Manual - Payment Due" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:108 -msgid "Payment Retry Scheduled" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.purchase.receipt.php:110 -msgid "Purchase Receipt" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.quiz.failed.php:93 -msgid "Quiz Failed" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.quiz.passed.php:93 -msgid "Quiz Passed" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.section.complete.php:86 -msgid "Section Complete" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.student.welcome.php:95 -msgid "Student Welcome" -msgstr "" - -#: includes/notifications/controllers/class.llms.notification.controller.subscription.cancelled.php:88, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:131 -msgid "Subscription Cancellation Notice" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:78 -msgid "Achievement Content" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:79 -msgid "Achievement Image" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:80 -msgid "Achievement Image URL" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:81 -msgid "Achievement Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:82, -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:110, -#: includes/notifications/views/class.llms.notification.view.course.complete.php:77, -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:77, -#: includes/notifications/views/class.llms.notification.view.enrollment.php:75, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:81, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:84, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:84, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:79, -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:69 -msgid "Student Name" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:106 -msgctxt "Achievement icon alt text" -msgid "%s Icon" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:121, -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:145, -#: includes/notifications/views/class.llms.notification.view.course.complete.php:97, -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:98, -#: includes/notifications/views/class.llms.notification.view.enrollment.php:99, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:115, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:135, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:135, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:113 -msgid "you" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:147 -msgid "You've been awarded an achievement!" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:86 -msgid "View Full Certificate" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:107 -msgid "Certificate Content" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:108, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:45, -#: includes/admin/reporting/tables/llms.table.certificates.php:157 -msgid "Certificate Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:109 -msgid "Certificate URL" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:111 -msgid "Mini Certificate" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:171 -msgid "You've earned a certificate!" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.complete.php:43, -#: includes/notifications/views/class.llms.notification.view.course.complete.php:113, -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:43, -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:114, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:43, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:131, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:43, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:129 -msgid "Congratulations! %1$s completed %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.complete.php:45, -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:45, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:45, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:45 -msgid "Congratulations! You finished %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.complete.php:76, -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:79, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:79, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:79, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:77 -msgid "Course Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.complete.php:123 -msgid "%s Completed a Course" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:76 -msgid "Track Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:124 -msgid "%s Completed a Track" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.enrollment.php:42 -msgid "Congratulations! %1$s enrolled in %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.enrollment.php:74 -msgid "Type (Course or Membership)" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.enrollment.php:115 -msgid "%1$s enrolled in %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.enrollment.php:125 -msgid "%1$s enrollment success!" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:78, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:78, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:78, -#: includes/notifications/views/class.llms.notification.view.section.complete.php:76 -msgid "Course Progress Bar" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:80, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:82, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:82, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:39 -msgid "Lesson Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:141 -msgid "%s Completed a Lesson" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:57 -msgid "Head over to your dashboard for payment instructions." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:78, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:78 -msgid "Payment Due Date" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:85, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:85, -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:28 -msgid "Hello %s," -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:86 -msgid "A payment for your subscription to %1$s is due." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:87 -msgid "Sign in to your account and %1$spay now%2$s." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:88, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:88, -#: templates/admin/post-types/order-details.php:29 -msgid "Order #%s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:97 -msgid "Pay Invoice" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:110 -msgid "Pay Now" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:131, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:131, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:90 -msgid "Customer Address" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:132, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:132, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:91, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:63 -msgid "Customer Name" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:133, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:133, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:92 -msgid "Customer Phone" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:135, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:135, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:93, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:64 -msgid "Order ID" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:136, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:136, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:94 -msgid "Order URL" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:137, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:137 -msgid "Payment Amount" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:138, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:138, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:95, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:65, -#: templates/admin/post-types/product-access-plan.php:61 -msgid "Plan Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:139, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:139, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:96, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:66 -msgid "Product Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:140, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:140, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:97, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:67 -msgid "Product Type" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:141, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:141, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:98, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:68 -msgid "Product Title (Link)" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:219, -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:219, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:175, -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:114 -msgctxt "generic product type description" -msgid "Item" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:238, -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:251 -msgid "A payment is due for your subscription to %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:249 -msgid "Payment Due for Order #%s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:57 -msgid "" -"Head over to the order to see what went wrong and update your payment method " -"to reactivate your subscription." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:86 -msgid "" -"The automatic payment for your subscription to %1$s has failed. We'll " -"automatically retry this charge on %2$s." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:87 -msgid "" -"To reactivate your subscription you can login to your account and %1$spay now" -"%2$s." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:238 -msgid "Automatic payment for %1$s failed, retry scheduled for %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:249 -msgid "Automatic payment failed for order #%s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.payment.retry.php:251 -msgid "An automatic payment failed for your subscription to %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:44, -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:101, -#: templates/admin/post-types/order-transactions.php:25 -msgid "Transaction ID" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:56 -msgid "View Order Details" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:99 -msgid "Transaction Amount" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:100 -msgid "Transaction Date" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:102 -msgid "Transaction Source" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:208 -msgid "Purchase Receipt for %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:218 -msgid "Purchase Receipt for Order #%s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:43, -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:151 -msgid "%1$s failed %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:45 -msgid "You failed %s!" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:81, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:81 -msgid "Grade Bar" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:83, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:83 -msgid "Quiz Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:161 -msgid "%s failed a quiz" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:43, -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:151 -msgid "Congratulations! %1$s passed %2$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:45 -msgid "Congratulations! You passed %s!" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:161 -msgid "%s passed a quiz" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.section.complete.php:78 -msgid "Section Title" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.section.complete.php:139 -msgid "%s Completed a Section" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:29 -msgid "Here's some helpful information to help you get started at %s." -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:30 -msgid "Your Login" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:31 -msgid "Your Dashboard" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:32 -msgid "" -"If you forgot or don't have a password you can reset it now so you can login " -"and get started:" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:66 -msgid "Dashboard URL" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:67 -msgid "Password Reset URL" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:70 -msgid "Student Login" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:119 -msgid "Welcome to %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.student.welcome.php:129 -msgid "Let's get started %s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:26 -msgid "%1$s has cancelled their subscription (#%2$s) to the %3$s %4$s" -msgstr "" - -#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:141 -msgid "%1$s subscription cancellation" -msgstr "" - -#: templates/admin/analytics/analytics.php:45, -#: templates/admin/reporting/nav-filters.php:27 -msgid "Last 7 Days" -msgstr "" - -#: templates/admin/analytics/analytics.php:51, -#: templates/admin/reporting/nav-filters.php:33 -msgid "Custom" -msgstr "" - -#: templates/admin/analytics/analytics.php:55, -#: templates/admin/reporting/nav-filters.php:37 -msgid "Go" -msgstr "" - -#: templates/admin/analytics/analytics.php:59, -#: templates/admin/reporting/nav-filters.php:41 -msgid "Toggle Filters" -msgstr "" - -#: templates/admin/analytics/analytics.php:92, -#: templates/admin/reporting/nav-filters.php:74 -msgid "Filter by Course(s)" -msgstr "" - -#: templates/admin/analytics/analytics.php:94, -#: templates/admin/analytics/analytics.php:106, -#: templates/admin/reporting/nav-filters.php:76, -#: templates/admin/reporting/nav-filters.php:88 -msgid "(ID# %d)" -msgstr "" - -#: templates/admin/analytics/analytics.php:104, -#: templates/admin/reporting/nav-filters.php:86 -msgid "Filter by Memberships(s)" -msgstr "" - -#: templates/admin/analytics/analytics.php:113, -#: templates/admin/reporting/nav-filters.php:95 -msgid "Apply Filters" -msgstr "" - -#: templates/admin/import/import.php:12 -msgid "LifterLMS Importer" -msgstr "" - -#: templates/admin/import/import.php:21 -msgid "Import Course(s)" -msgstr "" - -#: templates/admin/import/import.php:24 -msgid "Upload export files generated by LifterLMS. Must be a \".json\" file." -msgstr "" - -#: templates/admin/notices/db-update.php:11 -msgid "The LifterLMS database needs to be updated to the latest version." -msgstr "" - -#: templates/admin/notices/db-update.php:12 -msgid "" -"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." -msgstr "" - -#: templates/admin/notices/db-update.php:13 -msgid "" -"See the %1$sdatabase update log%2$s for a complete list of changes scheduled " -"for each upgrade." -msgstr "" - -#: templates/admin/notices/db-update.php:14 -msgid "Run the Updater" -msgstr "" - -#: templates/admin/notices/db-update.php:18 -msgid "" -"We strongly recommended that you backup your database before proceeding. Are " -"you sure you wish to run the updater now?" -msgstr "" - -#: templates/admin/notices/db-updating.php:13 -msgid "LifterLMS database update" -msgstr "" - -#: templates/admin/notices/db-updating.php:13 -msgid "Your database is being upgraded in the background." -msgstr "" - -#: templates/admin/notices/db-updating.php:14 -msgid "Click here for database update FAQs" -msgstr "" - -#: templates/admin/notices/db-updating.php:16 -msgid "Taking too long? Click here to run the update now." -msgstr "" - -#: templates/admin/notices/staging.php:12 -msgid "It looks like you may have installed LifterLMS on a staging site!" -msgstr "" - -#: templates/admin/notices/staging.php:14 -msgid "" -"LifterLMS watches for potential signs of a staging site and disables " -"automatic payments so that your students do not receive duplicate charges." -msgstr "" - -#: templates/admin/notices/staging.php:17 -msgid "" -"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." -msgstr "" - -#: templates/admin/notices/staging.php:22 -msgid "Leave Automatic Payments Disabled" -msgstr "" - -#: templates/admin/notices/staging.php:24 -msgid "Enable Automatic Payments" -msgstr "" - -#: templates/admin/post-types/order-details.php:23 -msgid "This order was processed in the gateway's testing mode" -msgstr "" - -#: templates/admin/post-types/order-details.php:30 -msgid "Processed by %s" -msgstr "" - -#: templates/admin/post-types/order-details.php:48 -msgid "Access Plan Information" -msgstr "" - -#: templates/admin/post-types/order-details.php:51, -#: templates/admin/post-types/order-details.php:70, -#: templates/admin/post-types/order-details.php:220 -msgid "Name:" -msgstr "" - -#: templates/admin/post-types/order-details.php:57, -#: templates/admin/post-types/order-details.php:76 -msgid "SKU:" -msgstr "" - -#: templates/admin/post-types/order-details.php:67 -msgid "Product Information" -msgstr "" - -#: templates/admin/post-types/order-details.php:90 -msgid "Trial Information" -msgstr "" - -#: templates/admin/post-types/order-details.php:94, -#: templates/admin/post-types/order-details.php:118 -msgid "Original Total:" -msgstr "" - -#: templates/admin/post-types/order-details.php:99, -#: templates/admin/post-types/order-details.php:132 -msgid "Coupon Discount:" -msgstr "" - -#: templates/admin/post-types/order-details.php:107, -#: templates/admin/post-types/order-details.php:141 -msgid "Total:" -msgstr "" - -#: templates/admin/post-types/order-details.php:114 -msgid "Payment Information" -msgstr "" - -#: templates/admin/post-types/order-details.php:124 -msgid "Sale Discount:" -msgstr "" - -#: templates/admin/post-types/order-details.php:161 -msgid "Customer Information" -msgstr "" - -#: templates/admin/post-types/order-details.php:164 -msgid "Buyer Name:" -msgstr "" - -#: templates/admin/post-types/order-details.php:173 -msgid "Buyer Email:" -msgstr "" - -#: templates/admin/post-types/order-details.php:179 -msgid "Buyer Address:" -msgstr "" - -#: templates/admin/post-types/order-details.php:193 -msgid "Buyer Phone:" -msgstr "" - -#: templates/admin/post-types/order-details.php:200 -msgid "Buyer IP Address:" -msgstr "" - -#: templates/admin/post-types/order-details.php:217 -msgid "Gateway Information" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:21 -msgid "Refunded" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:22, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:38 -msgid "Type" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:24 -msgid "Source" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:26 -msgid "Actions" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:78 -msgid "Resend Receipt" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:89, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:85 -msgid "%s Newer" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:93, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:89 -msgid "Older %s" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:97 -msgid "View all" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:101 -msgid "Show Less Info" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:101 -msgid "Show More Info" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:114 -msgid "Refund Amount:" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:119 -msgid "Refund Note (optional):" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:124 -msgid "" -"The refund will be recorded and you will need to manually issue a refund" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:125 -msgctxt "refund via payment gateway" -msgid "Refund via %s" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:146 -msgid "Payment Amount:" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:151 -msgid "Payment Source (optional):" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:156 -msgid "Payment Transaction ID (optional):" -msgstr "" - -#: templates/admin/post-types/order-transactions.php:161 -msgid "Payment Note (optional):" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:41 -msgid "Unnamed Access Plan" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:42 -msgctxt "Product Access Plan ID" -msgid "ID# %s" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:43 -msgid "Purchase Link" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:66 -msgid "Plan SKU" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:71 -msgid "Enroll Text" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:76, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:82 -msgid "Visibility" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:85 -msgid "Is Free" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:87 -msgid "No payment required" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:97 -msgid "Price" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:102 -msgid "Frequency" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:104 -msgid "one-time payment" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:105 -msgid "every" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:106 -msgid "every 2nd" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:107 -msgid "every 3rd" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:108 -msgid "every 4th" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:109 -msgid "every 5th" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:110 -msgid "every 6th" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:119 -msgid "Plan Period" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:123 -msgid "week" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:129 -msgid "Plan Length" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:131, -#: templates/admin/post-types/product-access-plan.php:139, -#: templates/admin/post-types/product-access-plan.php:147, -#: templates/admin/post-types/product-access-plan.php:155 -msgid "for all time" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:133 -msgid "for %s year" -msgid_plural "for %s years" -msgstr[0] "" -msgstr[1] "" - -#: templates/admin/post-types/product-access-plan.php:141 -msgid "for %s month" -msgid_plural "for %s months" -msgstr[0] "" -msgstr[1] "" - -#: templates/admin/post-types/product-access-plan.php:149 -msgid "for %s week" -msgid_plural "for %s weeks" -msgstr[0] "" -msgstr[1] "" - -#: templates/admin/post-types/product-access-plan.php:157 -msgid "for %s day" -msgid_plural "for %s days" -msgstr[0] "" -msgstr[1] "" - -#: templates/admin/post-types/product-access-plan.php:174, -#: includes/admin/views/metaboxes/view-order-submit.php:80 -msgid "Access Expiration" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:177 -msgid "Expires after" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:178 -msgid "Expires on" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:195, -#: templates/admin/post-types/product-access-plan.php:257 -msgid "year(s)" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:196, -#: templates/admin/post-types/product-access-plan.php:258 -msgid "month(s)" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:197, -#: templates/admin/post-types/product-access-plan.php:259 -msgid "week(s)" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:198, -#: templates/admin/post-types/product-access-plan.php:260 -msgid "day(s)" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:209 -msgid "Plan Availability" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:211 -msgid "Anyone" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:212 -msgid "Members only" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:221 -msgid "ID# %d" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:237 -msgid "Trial Offer" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:239 -msgid "No trial offer" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:240 -msgid "Enable trial" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:245 -msgid "Trial Price" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:250 -msgid "Trial Length" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:271 -msgid "Sale Pricing" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:273 -msgid "Not on sale" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:274 -msgid "On Sale" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:279 -msgid "Sale Price" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:284 -msgid "Sale Start Date" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:289 -msgid "Sale End Date" -msgstr "" - -#: templates/admin/post-types/product-access-plan.php:299 -msgid "Plan Description" -msgstr "" - -#: templates/admin/post-types/product.php:18 -msgid "%s Access Plans" -msgstr "" - -#: templates/admin/post-types/product.php:19 -msgid "" -"Access plans define the payment options available for this %s during checkout" -msgstr "" - -#: templates/admin/post-types/product.php:25, -#: templates/admin/post-types/product.php:44 -msgid "You cannot create more than %d access plans for each product." -msgstr "" - -#: templates/admin/post-types/product.php:33 -msgid "No access plans exist for your %s." -msgstr "" - -#: templates/admin/post-types/product.php:43 -msgid "Save Access Plans" -msgstr "" - -#: templates/admin/post-types/product.php:54 -msgid "Confirm Your Action" -msgstr "" - -#: templates/admin/post-types/product.php:57 -msgid "" -"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." -msgstr "" - -#: templates/admin/post-types/product.php:58 -msgid "This action cannot be reversed. " -msgstr "" - -#: templates/admin/post-types/product.php:59 -msgid "Press the \"Delete\" button to permanently remove this plan." -msgstr "" - -#: templates/admin/post-types/product.php:60, -#: includes/admin/reporting/tables/llms.table.achievements.php:37, -#: includes/admin/reporting/tables/llms.table.certificates.php:47 -msgid "Delete" -msgstr "" - -#: templates/admin/post-types/students.php:26 -msgid "Enroll New Students" -msgstr "" - -#: templates/admin/post-types/students.php:33 -msgid "Enroll Students" -msgstr "" - -#: templates/admin/reporting/reporting.php:43 -msgid "LifterLMS Reporting Beta" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:25, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:50 -msgid "Membership Access" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:56 -msgctxt "apply membership restriction to post type" -msgid "Restrict this %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:65 -msgid "Visitors must belong to one of these memberships to access this %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:25 -msgid "Achievement Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:50, -#: includes/admin/reporting/tables/llms.table.achievements.php:153 -msgid "Achievement Title" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:51 -msgid "Enter a title for your achievement. IE: Achievement of Completion" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:62 -msgid "Achievement Content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:63 -msgid "Enter any information you would like to display on the achievement." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:74, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:56 -msgid "Background Image" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:75 -msgid "Select an Image to use for the achievement." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:21 -msgid "Certificate Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:46 -msgid "Enter a title for your certificate. EG: Certificate of Completion" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:57 -msgid "Select an Image to use for the certificate." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:20 -msgid "Coupon Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:74 -msgid "Select a dollar or percentage discount." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:77 -msgid "Discount Type" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:86 -msgid "%s Discount" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:92 -msgid "Access Plan Types" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:93 -msgid "Select which type of access plans this coupon can be used with." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:99 -msgid "Any Access Plan" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:103 -msgid "Only One-time Payment Access Plans" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:107 -msgid "Only Recurring Access Plans" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:118 -msgid "Discount Amount" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:119 -msgid "" -"The amount to be subtracted from the \"Price\" of an applicable access plan. " -"Do not include symbols such as %1$s." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:136 -msgid "Trial Discount Amount" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:137 -msgid "" -"The amount to be subtracted from the \"Trial Price\" of an applicable access " -"plan. Do not include symbols such as %1$s." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:148, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:166, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:147 -msgid "Restrictions" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:153 -msgid "Limit coupon to the following courses." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:166 -msgid "Limit coupon to the following memberships." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:179 -msgid "" -"Coupon will no longer be usable after this date. Leave blank for no " -"expiration." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:188 -msgid "Usage Limit" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:189 -msgid "" -"The amount of times this coupon can be used. Leave empty or enter 0 for " -"unlimited uses." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:205 -msgid "" -"Optional description for internal notes. This is never displayed to your " -"students." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:232 -msgid "" -"Coupon code already exists. Customers will use the most recently created " -"coupon with this code." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:241 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:74 -msgid "This lesson is not attached to a course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:93 -msgid "Course: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:103 -msgid "Launch Course Builder" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:19 -msgid "Course Options" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:63, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:91 -msgid "Sales Page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:68 -msgid "" -"Customize the content displayed to visitors and students who are not " -"enrolled in the course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:73, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:101 -msgid "Sales Page Content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:76 -msgid "Display default course content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:77, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:105 -msgid "Show custom content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:78, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:106 -msgid "Redirect to WordPress Page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:79, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:107 -msgid "Redirect to custom URL" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:85 -msgid "" -"This content will only be shown to visitors who are not enrolled in this " -"course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:87, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:115 -msgid "Sales Page Custom Content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:100, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:128 -msgid "Select a Page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:107, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:135 -msgid "Sales Page Redirect URL" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:122 -msgid "Course Length" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:123 -msgid "Enter a description of the estimated length. IE: 3 days" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:133 -msgid "" -"Choose a course difficulty level. New difficulties can be added via " -"%1$sCourses -> Difficulties%2$s." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:136 -msgid "Course Difficulty Category" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:143 -msgid "Featured Video" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:144, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:62 -msgid "" -"Paste the url for a Wistia, Vimeo or Youtube video or a hosted video file. " -"For a full list of supported providers see %s." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:149 -msgid "" -"When enabled, the featured video will be displayed on the course tile in " -"addition to the course page." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:152 -msgid "Display Featured Video on Course Tile" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:158 -msgid "Featured Audio" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:159, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:70 -msgid "" -"Paste the url for a SoundCloud or Spotify song or a hosted audio file. For a " -"full list of supported providers see %s." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:171 -msgid "You must enroll in this course to access course content." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:172 -msgid "" -"This message will be displayed when non-enrolled visitors attempt to access " -"course content directly without enrolling first" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:174 -msgid "Content Restricted Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:180 -msgid "Enable Enrollment Period" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:181 -msgid "Set registration start and end dates for this course" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:191 -msgid "Registration opens on this date." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:194 -msgid "Enrollment Start Date" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:201 -msgid "Registration closes on this date." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:204 -msgid "Enrollment End Date" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:211 -msgid "" -"Enrollment in this course opens on [lifterlms_course_info id=\"%d\" key=" -"\"enrollment_start_date\"]." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:212 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:214 -msgid "Enrollment Opens Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:221 -msgid "" -"Enrollment in this course closed on [lifterlms_course_info id=\"%d\" key=" -"\"enrollment_end_date\"]." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:222 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:224 -msgid "Enrollment Closed Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:230 -msgid "Enable Course Time Period" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:231 -msgid "" -"Set start and end dates for this course. Content can only be viewed and " -"completed within the selected range." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:243 -msgid "Course Start Date" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:252 -msgid "Course End Date" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:260 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:262 -msgid "Course Opens Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:270 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:272 -msgid "Course Closed Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:279, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:97 -msgid "Enable Prerequisite" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:280 -msgid "Enable to choose a prerequisite course or course track" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:292 -msgid "Select a course" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:295 -msgid "" -"Select a prerequisite course. Students must have completed the selected " -"course before they can view or complete content in this course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:298 -msgid "Choose Prerequisite Course" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:305 -msgid "" -"Select the prerequisite course track. Students must have completed the " -"select track before they can view or complete content in this course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:309 -msgid "Choose Prerequisite Course Track" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:316 -msgid "Enable Course Capacity" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:317 -msgid "Limit the number of users that can enroll in this course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:330 -msgid "Course Capacity" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:337 -msgid "" -"This message will be displayed to non-enrolled visitors once the Course " -"Capacity has been reached. " -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:339 -msgid "Capacity Reached Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:41 -msgid "Admin Email" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:50 -msgid "Email Subject" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:51 -msgid "This will be used for the subject line of your email." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:60 -msgid "Email Heading" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:61 -msgid "This is the heading for your email. It will display above the content." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:70 -msgid "Email To:" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:71, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:82, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:91 -msgid "Separate multiple address with a comma." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:81 -msgid "Email CC:" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:90 -msgid "Email BCC:" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:21 -msgid "Engagement Options" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:46 -msgid "" -"This engagement will be triggered when a student completes the selected " -"action" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:50 -msgid "Triggering Event" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:69 -msgid "Select a Lesson" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:77 -msgid "Select an Access Plan" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:96 -msgid "Select a Quiz" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:102 -msgid "Select a Section" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:153, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:156 -msgid "Select a Course Track" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:165 -msgid "Determines the type of engagement" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:168 -msgid "Engagement Type" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:181, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:185 -msgid "Select an Engagement" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:193 -msgid "" -"Enter the number of days to wait before triggering this engagement. Enter 0 " -"or leave blank to trigger immediately." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:195 -msgid "Engagement Delay" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:202 -msgid "Engagement Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.expiration.php:67 -msgid "Please select an option..." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:22, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:41, -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.instructors.php:49, -#: includes/admin/reporting/tables/llms.table.courses.php:282 -msgid "Instructors" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:45 -msgid "Add Instructor" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:49 -msgid "New Instructor" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:58 -msgid "Select an Instructor" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:73 -msgid "Label" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:23 -msgid "Lesson Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:45 -msgid "After course enrollment" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:46 -msgid "After course start date" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:53 -msgid "After prerequisite completion" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:65 -msgid "Video Embed Url" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:74 -msgid "Audio Embed Url" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:78 -msgid "" -"Checking this box will allow guests to view the content of this lesson " -"without registering or signing up for the course." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:89 -msgid "Prerequisites" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:94 -msgid "Enable to choose a prerequisite Lesson" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:105 -msgid "Select a Prerequisite Lesson" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:108 -msgid "Select the prerequisite lesson" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:111 -msgid "Choose Prerequisite" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:118 -msgid "Drip Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:134 -msgid "Delay (in days) " -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:144 -msgid "Date Available" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:151 -msgid "" -"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" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:153 -msgid "Time Available" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:164 -msgid "" -"Checking this box will require students to get a passing score on the above " -"quiz to complete the lesson." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:167 -msgid "Require Passing Grade" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:49 -msgid "Remove from Auto-enrollment" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:50 -msgid "Enroll All Members" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:96 -msgid "" -"Customize the content displayed to visitors and students who are not " -"enrolled in the membership." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:104 -msgid "Display default membership content" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:113 -msgid "" -"This content will only be shown to visitors who are not enrolled in this " -"membership." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:152 -msgid "" -"When a non-member attempts to access content restricted to this membership" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:156 -msgid "Restricted Access Redirect" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:160 -msgid "Stay on page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:164 -msgid "Redirect to this membership page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:168 -msgid "Redirect to a WordPress page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:172 -msgid "Redirect to a Custom URL" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:184 -msgid "Select a WordPress Page" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:194 -msgid "Enter a Custom URL" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:202 -msgid "" -"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." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:205 -msgid "Display a Message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:211 -msgid "Shortcodes like %s can be used in this message" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:212 -msgid "You must belong to the %s membership to access this content." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:214 -msgid "Restricted Content Notice" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:221, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:224 -msgid "Auto Enrollment" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:225 -msgid "" -"When a student joins this membership they will be automatically enrolled in " -"these courses. Click %1$shere%2$s for more information." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:227 -msgid "Course Name" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:228 -msgid "No auto-enrollment courses found." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:237 -msgid "Select course(s)" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:240 -msgid "" -"When a member is enrolled in this membership they will be automatically " -"enrolled into any courses in the auto-enrollment list" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:242 -msgid "Add Auto-enrollment Course(s)" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.details.php:22 -msgid "Order Details" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:51 -msgid "Cannot manage enrollment status for anonymized orders." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:63 -msgid "Select" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:70 -msgctxt "enrollment status" -msgid "Status: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:75 -msgctxt "enrollment trigger" -msgid "Enrolled: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:78 -msgctxt "enrollment trigger" -msgid "Updated: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:81 -msgctxt "enrollment trigger" -msgid "Trigger: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:87 -msgid "Update Enrollment Status" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:109 -msgid "Student enrollment status changed from %1$s to %2$s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:19 -msgid "Order Notes" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:68 -msgctxt "order note author" -msgid "by %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:69 -msgctxt "order note date" -msgid "on %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:93 -msgid "No order notes found." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.submit.php:20 -msgid "Order Information" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.submit.php:67 -msgid "The status of a Legacy order cannot be changed." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:127 -msgid "Refund Error: Missing a transaction ID" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:129 -msgid "Refund Error: Missing or invalid refund amount" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:137, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:170 -msgctxt "admin error message" -msgid "Refund Error: %s" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:151 -msgid "Refund Error: Missing or invalid payment amount" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:21 -msgid "Product Options" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:126 -msgid "Access Plan data was posted in an invalid format" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:134 -msgid "Access Plan title is required" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:138 -msgid "Access Plan price is required" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:142 -msgid "Sale price is required if the plan is on sale" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:146 -msgid "Trial price is required if the plan has a trial" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.students.php:29 -msgid "Student Management" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.students.php:60 -msgid "You must publish this post before you can manage students." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:36 -msgid "Video Embed Code" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:44 -msgid "Paste the url for your Wistia, Vimeo or Youtube videos." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:54 -msgid "Audio Embed Code" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:62 -msgid "Paste the embed code for your externally hosted audio." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:49 -msgid "Catalog visibility:" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:55 -msgid "" -"Choose the visibility of the %s in your catalog. It will always be available " -"directly." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:61 -msgid "OK" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:25 -msgid "You need to publish this post before you can generate a CSV." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:34 -msgid "Vouchers only" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:35 -msgid "Generates a CSV of voucher codes, uses, and remaining uses." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:40 -msgid "Redeemed codes" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:41 -msgid "Generated a CSV of student emails, redemption date, and used code." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:48 -msgid "Email CSV" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:50 -msgid "Send to multiple emails by separating emails addresses with commas." -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:53 -msgid "Generate Export" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:18 -msgid "Voucher Settings" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:72 -msgid "Codes" -msgstr "" - -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:80, -#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:84 -msgid "Redemptions" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:34 -msgid "Coupon Amount" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:36 -msgid "Usage / Limit" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:59 -msgid "Discount: " -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:64 -msgid "Trial Discount: " -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:45 -msgid "Builder" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:84 -msgid "courses export" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:37 -msgid "Trigger" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:39 -msgid "Delay" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:114 -msgid "%d days" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:42 -msgid "Payment Status" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:43 -msgid "Access Status" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:45 -msgid "Revenue" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:71 -msgctxt "order number display" -msgid "#%d" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:74 -msgid "by" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:102 -msgctxt "access plan expiration" -msgid "Expired:" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:104 -msgctxt "access plan expiration" -msgid "Expires:" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:25 -msgid "LifterLMS Checkout" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:26 -msgid "LifterLMS Course Catalog" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:27 -msgid "LifterLMS Memberships Catalog" -msgstr "" - -#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:28 -msgid "LifterLMS Student Dashboard" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:88 -msgid "Visit the triggering order to manage this student's enrollment" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:91 -msgid "Cancel Enrollment" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:96 -msgid "Reactivate Enrollment" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:164 -msgid "Admin: %1$s (#%2$d)" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:220, -#: includes/admin/reporting/tables/llms.table.course.students.php:240, -#: includes/admin/reporting/tables/llms.table.students.php:247 -msgid "Search students by name or email..." -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:232 -msgid "Manage Existing Enrollments" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:351, -#: includes/admin/reporting/tables/llms.table.course.students.php:372, -#: includes/admin/reporting/tables/llms.table.student.course.php:185, -#: includes/admin/reporting/tables/llms.table.student.courses.php:199, -#: includes/admin/reporting/tables/llms.table.student.memberships.php:107, -#: includes/admin/reporting/tables/llms.table.students.php:404 -msgid "Name" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:360, -#: includes/admin/reporting/tables/llms.table.course.students.php:398 -msgid "Enrollment Updated" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:372, -#: includes/admin/reporting/tables/llms.table.course.students.php:417 -msgid "Last Lesson" -msgstr "" - -#: includes/admin/post-types/tables/class.llms.table.student.management.php:376 -msgid "Enrollment Trigger" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.achievements.php:47 -msgid "" -"Are you sure you want to delete this achievement? This action cannot be " -"undone!" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.achievements.php:152, -#: includes/admin/reporting/tables/llms.table.certificates.php:156 -msgid "Template ID" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.achievements.php:156, -#: includes/admin/reporting/tables/llms.table.certificates.php:159 -msgid "Related Post" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.achievements.php:168 -msgid "This student has not yet earned any achievements." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.certificates.php:42 -msgid "Download" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.certificates.php:57 -msgid "" -"Are you sure you want to delete this certificate? This action cannot be " -"undone!" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.certificates.php:171 -msgid "This student has not yet earned any certificates." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.course.students.php:403, -#: includes/admin/reporting/tables/llms.table.student.course.php:194, -#: includes/admin/reporting/tables/llms.table.student.courses.php:216 -msgid "Completed" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.courses.php:248 -msgid "Search courses..." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.courses.php:290 -msgid "Average Progress" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.courses.php:295, -#: includes/admin/reporting/tables/llms.table.quizzes.php:320 -msgid "Average Grade" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.questions.php:90 -msgid "Points" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.questions.php:92 -msgid "Selected Answer" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.questions.php:93 -msgid "Correct Answer" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:252 -msgid "Attempt #" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:273, -#: templates/admin/reporting/tabs/quizzes/attempt.php:94 -msgid "End Date" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.quizzes.php:272 -msgid "Search quizzes..." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.quizzes.php:315 -msgid "Total Attempts" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.student.course.php:143 -msgctxt "section title" -msgid "Section: %s" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.student.courses.php:212 -msgid "Updated" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.student.courses.php:228 -msgid "This student is not enrolled in any courses." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.student.memberships.php:125 -msgid "This student is not enrolled in any memberships." -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:419 -msgid "Registration Date" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:437 -msgid "Completions" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:474 -msgid "Billing Zip" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:489 -msgid "Courses (Enrolled)" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:494 -msgid "Courses (Cancelled)" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:499 -msgid "Courses (Expired)" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:504 -msgid "Memberships (Enrolled)" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:509 -msgid "Memberships (Cancelled)" -msgstr "" - -#: includes/admin/reporting/tables/llms.table.students.php:514 -msgid "Memberships (Expired)" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:90, -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:71 -msgid "Overview" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:66 -msgid "Courses Completed" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:69 -msgid "Number of total courses completed during the selected period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:72 -msgid "Attempts" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:54, -#: includes/admin/reporting/widgets/class.llms.analytics.widget.sales.php:23 -msgid "# of Sales" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:57 -msgid "Number of new active or completed orders placed within this period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:66, -#: includes/admin/reporting/widgets/class.llms.analytics.widget.refunds.php:25 -msgid "# of Refunds" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:69 -msgid "Number of orders refunded during this period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:72, -#: includes/admin/reporting/widgets/class.llms.analytics.widget.refunded.php:24 -msgid "Amount Refunded" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:75 -msgid "Total of all transactions refunded during this period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:86, -#: includes/admin/reporting/widgets/class.llms.analytics.widget.coupons.php:26 -msgid "# of Coupons Used" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:89 -msgid "Number of orders completed using coupons during this period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:92 -msgid "Amount of Coupons" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:95 -msgid "Total amount of coupons used during this period" -msgstr "" - -#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:82 -msgid "Information" -msgstr "" - -#: includes/admin/reporting/widgets/class.llms.analytics.widget.coursecompletions.php:19 -msgid "# of Courses Completed" -msgstr "" - -#: includes/admin/reporting/widgets/class.llms.analytics.widget.enrollments.php:20 -msgid "# of Enrollments" -msgstr "" - -#: includes/admin/reporting/widgets/class.llms.analytics.widget.lessoncompletions.php:19 -msgid "# of Lessons Completed" -msgstr "" - -#: includes/admin/reporting/widgets/class.llms.analytics.widget.registrations.php:19 -msgid "# of Registrations" -msgstr "" - -#: includes/admin/settings/tables/class.llms.table.notification.settings.php:102 -msgid "Notification" -msgstr "" - -#: includes/admin/settings/tables/class.llms.table.notification.settings.php:103 -msgid "Configure" -msgstr "" - -#: includes/admin/views/builder/assignment.php:15 -msgid "There's no assignment associated with this lesson." -msgstr "" - -#: includes/admin/views/builder/assignment.php:18 -msgid "Create New Assignment" -msgstr "" - -#: includes/admin/views/builder/course.php:18, -#: includes/admin/views/builder/course.php:20 -msgid "Open WordPress course editor" -msgstr "" - -#: includes/admin/views/builder/course.php:23 -msgid "View course" -msgstr "" - -#: includes/admin/views/builder/editor.php:23 -msgid "Assignment" -msgstr "" - -#: includes/admin/views/builder/editor.php:32 -msgid "Close" -msgstr "" - -#: includes/admin/views/builder/elements.php:10 -msgid "Add Elements" -msgstr "" - -#: includes/admin/views/builder/elements.php:27 -msgid "Existing Lesson" -msgstr "" - -#: includes/admin/views/builder/lesson-settings.php:17, -#: includes/admin/views/builder/quiz.php:43 -msgid "Published" -msgstr "" - -#: includes/admin/views/builder/lesson-settings.php:25, -#: includes/admin/views/builder/lesson-settings.php:27, -#: includes/admin/views/builder/lesson.php:21 -msgid "Open WordPress lesson editor" -msgstr "" - -#: includes/admin/views/builder/lesson-settings.php:30, -#: includes/admin/views/builder/lesson-settings.php:32, -#: includes/admin/views/builder/lesson.php:49 -msgid "Detach Lesson" -msgstr "" - -#: includes/admin/views/builder/lesson-settings.php:36, -#: includes/admin/views/builder/lesson-settings.php:38 -msgid "Delete Lesson" -msgstr "" - -#: includes/admin/views/builder/lesson.php:27 -msgid "View lesson" -msgstr "" - -#: includes/admin/views/builder/lesson.php:32, -#: includes/admin/views/builder/section.php:33 -msgid "Shift up" -msgstr "" - -#: includes/admin/views/builder/lesson.php:36, -#: includes/admin/views/builder/section.php:37 -msgid "Shift down" -msgstr "" - -#: includes/admin/views/builder/lesson.php:40 -msgid "Move to previous section" -msgstr "" - -#: includes/admin/views/builder/lesson.php:44 -msgid "Move to next section" -msgstr "" - -#: includes/admin/views/builder/lesson.php:55 -msgid "Trash Lesson" -msgstr "" - -#: includes/admin/views/builder/lesson.php:72 -msgid "Edit Lesson settings" -msgstr "" - -#: includes/admin/views/builder/lesson.php:81 -msgid "Add an assignment" -msgstr "" - -#: includes/admin/views/builder/lesson.php:82 -msgid "Edit Assignment: %s" -msgstr "" - -#: includes/admin/views/builder/lesson.php:90 -msgid "Add a quiz" -msgstr "" - -#: includes/admin/views/builder/lesson.php:91 -msgid "Edit Quiz: %s" -msgstr "" - -#: includes/admin/views/builder/lesson.php:99 -msgid "No content" -msgstr "" - -#: includes/admin/views/builder/lesson.php:100 -msgid "Has content" -msgstr "" - -#: includes/admin/views/builder/lesson.php:108 -msgid "No video" -msgstr "" - -#: includes/admin/views/builder/lesson.php:109 -msgid "Has video" -msgstr "" - -#: includes/admin/views/builder/lesson.php:117 -msgid "No audio" -msgstr "" - -#: includes/admin/views/builder/lesson.php:118 -msgid "Has audio" -msgstr "" - -#: includes/admin/views/builder/lesson.php:126 -msgid "Enrolled students only" -msgstr "" - -#: includes/admin/views/builder/lesson.php:135 -msgid "No prerequisite" -msgstr "" - -#: includes/admin/views/builder/lesson.php:136 -msgid "Prerequisite Enabled" -msgstr "" - -#: includes/admin/views/builder/lesson.php:144 -msgid "Drip disabled" -msgstr "" - -#: includes/admin/views/builder/lesson.php:145 -msgid "Drip Enabled" -msgstr "" - -#: includes/admin/views/builder/question-choice.php:22 -msgid "Enter a choice..." -msgstr "" - -#: includes/admin/views/builder/question-choice.php:28, -#: includes/admin/views/builder/question-choice.php:30, -#: includes/admin/views/builder/question.php:87, -#: includes/admin/views/builder/question.php:89 -msgid "Remove image" -msgstr "" - -#: includes/admin/views/builder/question-choice.php:32, -#: includes/admin/views/builder/question.php:91 -msgid "image preview" -msgstr "" - -#: includes/admin/views/builder/question-choice.php:36, -#: includes/admin/views/builder/question.php:95 -msgid "Add Image" -msgstr "" - -#: includes/admin/views/builder/question-choice.php:44, -#: includes/admin/views/builder/question-choice.php:46 -msgid "Add Choice" -msgstr "" - -#: includes/admin/views/builder/question-choice.php:49, -#: includes/admin/views/builder/question-choice.php:51 -msgid "Delete Choice" -msgstr "" - -#: includes/admin/views/builder/question-type.php:11 -msgid "" -"Install the LifterLMS Advanced Quizzes add-on to enable this question type" -msgstr "" - -#: includes/admin/views/builder/question.php:24 -msgid "Expand question" -msgstr "" - -#: includes/admin/views/builder/question.php:28 -msgid "Collapse question" -msgstr "" - -#: includes/admin/views/builder/question.php:33 -msgid "Clone question" -msgstr "" - -#: includes/admin/views/builder/question.php:36 -msgid "Delete question" -msgstr "" - -#: includes/admin/views/builder/question.php:106 -msgid "Video" -msgstr "" - -#: includes/admin/views/builder/question.php:113 -msgid "https://" -msgstr "" - -#: includes/admin/views/builder/question.php:129 -msgid "Choices" -msgstr "" - -#: includes/admin/views/builder/question.php:133 -msgid "Multiple Correct Choices" -msgstr "" - -#: includes/admin/views/builder/question.php:144 -msgid "Drag a question here to add it to the group." -msgstr "" - -#: includes/admin/views/builder/question.php:153 -msgid "Result Clarifications" -msgstr "" - -#: includes/admin/views/builder/quiz.php:14 -msgid "There's no quiz associated with this lesson." -msgstr "" - -#: includes/admin/views/builder/quiz.php:17 -msgid "Create New Quiz" -msgstr "" - -#: includes/admin/views/builder/quiz.php:39 -msgid "Total Points" -msgstr "" - -#: includes/admin/views/builder/quiz.php:51, -#: includes/admin/views/builder/quiz.php:53 -msgid "Detach Quiz" -msgstr "" - -#: includes/admin/views/builder/quiz.php:57, -#: includes/admin/views/builder/quiz.php:59 -msgid "Delete Quiz" -msgstr "" - -#: includes/admin/views/builder/quiz.php:72 -msgid "Click \"Add Question\" below to start building your quiz!" -msgstr "" - -#: includes/admin/views/builder/quiz.php:77, -#: includes/admin/views/builder/utilities.php:20 -msgid "Collapse All" -msgstr "" - -#: includes/admin/views/builder/quiz.php:82, -#: includes/admin/views/builder/utilities.php:14 -msgid "Expand All" -msgstr "" - -#: includes/admin/views/builder/quiz.php:98 -msgid "Filter" -msgstr "" - -#: includes/admin/views/builder/section.php:22 -msgid "Expand section" -msgstr "" - -#: includes/admin/views/builder/section.php:28 -msgid "Collapse section" -msgstr "" - -#: includes/admin/views/builder/section.php:42 -msgid "Delete Section" -msgstr "" - -#: includes/admin/views/builder/sidebar.php:20 -msgid "Saved" -msgstr "" - -#: includes/admin/views/builder/sidebar.php:21 -msgid "Save changes" -msgstr "" - -#: includes/admin/views/builder/sidebar.php:22 -msgid "Saving changes..." -msgstr "" - -#: includes/admin/views/builder/sidebar.php:23 -msgid "Error saving changes..." -msgstr "" - -#: includes/admin/views/builder/sidebar.php:26 -msgid "Exit" -msgstr "" - -#: includes/admin/views/metaboxes/view-order-submit.php:22 -msgid "Update Order Status:" -msgstr "" - -#: includes/admin/views/metaboxes/view-order-submit.php:41 -msgid "Trial End Date" -msgstr "" - -#: includes/admin/views/metaboxes/view-order-submit.php:99 -msgid "Update Order" -msgstr "" - -#: includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.repeater.php:27 -msgid "Add New" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:27 -msgid "Course Overview" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:38 -msgid "Currently enrolled students" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:47 -msgid "Current average progress" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:56 -msgid "Current average grade" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:64 -msgid "New orders %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:73 -msgid "Total sales %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:82 -msgid "New enrollments %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:90 -msgid "Unenrollments %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:99 -msgid "Lessons completed %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:107 -msgid "Course completions %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:116 -msgid "Achievements earned %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:125 -msgid "Certificates earned %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:134 -msgid "Emails sent %s" -msgstr "" - -#: templates/admin/reporting/tabs/courses/overview.php:143, -#: templates/admin/reporting/tabs/quizzes/overview.php:77, -#: templates/admin/reporting/tabs/students/information.php:96 -msgid "Recent events" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:43 -msgid "Correct answers" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:52 -msgid "Points earned" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:102 -msgid "Time Elapsed" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:109 -msgid "Answers" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:120 -msgid "Start a Review" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:124 -msgid "Save Review" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:130 -msgid "Delete Attempt" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/attempt.php:144 -msgid "Additional Attempts" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:29 -msgid "Quiz Overview" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:41 -msgid "Attempts %s" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:51 -msgid "Average grade %s" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:59 -msgid "Passed attempts %s" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:67 -msgid "Failed attempts %s" -msgstr "" - -#: templates/admin/reporting/tabs/quizzes/overview.php:79 -msgid "Quiz events coming soon..." -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:26 -msgid "Enrollment Status" -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:40 -msgid "Current Grade" -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:48 -msgid "Enrollment Date" -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:56 -msgid "Completion Date" -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:70 -msgid "Progress: %s" -msgstr "" - -#: templates/admin/reporting/tabs/students/courses-course.php:71 -msgid "Grade: %s" -msgstr "" - -#: templates/admin/reporting/tabs/students/information.php:30 -msgid "Registered" -msgstr "" - -#: templates/admin/reporting/tabs/students/information.php:39 -msgid "Overall Progress" -msgstr "" - -#: templates/admin/reporting/tabs/students/information.php:48 -msgid "Overall Grade" -msgstr "" - -#: templates/admin/reporting/tabs/students/information.php:56 -msgid "Achievements earned" -msgstr "" - -#: templates/admin/reporting/tabs/students/information.php:64 -msgid "Certificates earned" -msgstr "" diff --git a/tests/assets/lifterlms-mock-addon.php b/tests/assets/lifterlms-mock-addon.php deleted file mode 100644 index 2f79737750..0000000000 --- a/tests/assets/lifterlms-mock-addon.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php -/** - * Plugin Name: LifterLMS Mock Add-on - * Plugin URI: https://lifterlms.com/ - * Description: Mock add-on plugin for phpunit integration tests related to add-ons. - * Version: 1.0.0 - * Author: LifterLMS - * Author URI: https://lifterlms.com/ - * License: GPLv3 - * License URI: https://www.gnu.org/licenses/gpl-3.0.html - */ - -defined( 'ABSPATH' ) || exit; - -define( 'LLMS_MOCK_PLUGIN_LOADED', true ); diff --git a/tests/assets/richard-i49WGMPd5aA-unsplash.jpg b/tests/assets/richard-i49WGMPd5aA-unsplash.jpg deleted file mode 100644 index cdc7f86802..0000000000 Binary files a/tests/assets/richard-i49WGMPd5aA-unsplash.jpg and /dev/null differ diff --git a/tests/assets/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg b/tests/assets/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg deleted file mode 100644 index 40cc8df2e3..0000000000 Binary files a/tests/assets/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg and /dev/null differ diff --git a/tests/bin/setup-e2e.sh b/tests/bin/setup-e2e.sh deleted file mode 100755 index 0535b212e9..0000000000 --- a/tests/bin/setup-e2e.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -llmsenv=vendor/bin/llms-env - -# 1. Activate LifterLMS plugin -############################## -$llmsenv wp plugin activate lifterlms - - -# 2. Bootstrap user accounts -############################ - -# StudentDashboard/RedeemVoucher -$llmsenv wp user create voucher voucher@email.tld --role=student --user_pass=password - -# StudentDashboardLogin -> should allow a user with valid credentials to login -# Settings/CopyPrevention -> StudentUser -$llmsenv wp user create validcreds validcreds@email.tld --role=student --user_pass=password - - -# 3. Set options. -################# - -$llmsenv wp option update can_compress_scripts 1 - - -# 4. Bootstrap posts -#################### - -# Settings/CopyPrevention -COPY_TEST_ID=$( $llmsenv wp post create --post_type=page --post_title="Integrity-Test" --post_status=publish --porcelain ) -$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 diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index 0635b5e63b..0000000000 --- a/tests/e2e/README.md +++ /dev/null @@ -1,79 +0,0 @@ -LifterLMS E2E (End-to-End) Tests -================================ - -## Requirements - -The E2E test suite requires [Node](https://nodejs.org/en/download/) to run tests via the terminal of your choosing. - -[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. - -**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.** - - -## Installation - -To install the test suite: - -+ `npm install`: Install Node dependencies. -+ `composer install`: Install all required PHP dependencies. -+ `composer run env up`: Build and install the local environment. -+ `composer run env:setup`: Setup the local environment. - -After installation a WordPress site should be accessible at [http://localhost:8080](http://localhost:8080) using the username `admin` and password `password`. - - -## Running Tests - -To run tests: - -+ `npm run test`: Runs all tests in a headless browser. -+ `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. -+ `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". -+ `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.". - - -## Managing Docker Containers - -The local environment is powered by docker containers which can be managed with the following commands: - -``` -config: Creates configuration override files -down: Stop and remove containers and volumes -up: Start containers -ps: List containers -reset: Destroy and recreate containers and volumes -restart: Restart containers -rm: Remove containers and volumes -ssh: Open an interactive bash session with the PHP service container -stop: Stop containers without removing them -wp: Execute a wp-cli command inside the PHP service container -``` - -To run these commands, run `composer run env <command>` where `<command>` is the name of the command you wish to run. - -For additionally information and options for each command run, the command with the `-h` or `--help` flag to view usage information. - - -## Test Organization - -All tests are stored in the [tests/e2e/tests](./tests) directory. - -Tests should organized into subdirectories by group and each file should function as a secondary level of organization for grouping tests. - - -## Credits - -Tools and libraries used: - -+ [Puppeteer](https://github.com/GoogleChrome/puppeteer): a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. -+ [Jest](https://github.com/facebook/jest): A comprehensive JavaScript testing solution. -+ [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer): A test runner to run tests using Jest & Puppeteer. -+ [expect-puppeteer](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/expect-puppeteer): Assertion library for Puppeteer. - -The following utility packages are used to help facilitate e2e tests in WordPress and LifterLMS: - -+ [@wordpress/scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts): A collection of reusable scripts tailored for WordPress development. -+ [@wordpress/e2e-test-utils](https://github.com/WordPress/gutenberg/tree/master/packages/e2e-test-utils): End-To-End (E2E) test utils for WordPress. -+ [llms-e2e-test-utils](https://github.com/gocodebox/lifterlms/tree/trunk/packages/llms-e2e-test-utils): End-To-End (E2E) test utils for LifterLMS. - -A 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... diff --git a/tests/e2e/tests/activate/setup-wizard.test.js b/tests/e2e/tests/activate/setup-wizard.test.js deleted file mode 100644 index 58e3bc7e85..0000000000 --- a/tests/e2e/tests/activate/setup-wizard.test.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Test the Setup Wizard - * - * @since 3.37.8 - * @since 3.37.14 Fix package references. - * @since 4.0.0-rc.1 Use `runSetupWizard()`. - */ - -import { visitPage, runSetupWizard } from '@lifterlms/llms-e2e-test-utils'; - -describe( 'SetupWizard', () => { - - it ( 'should load and run the entire setup wizard.', async () => { - await runSetupWizard(); - } ); - -} ); diff --git a/tests/e2e/tests/builder/builder.test.js b/tests/e2e/tests/builder/builder.test.js deleted file mode 100644 index 5eac2f7465..0000000000 --- a/tests/e2e/tests/builder/builder.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { - clickAndWait, - clickElementByText, - fillField, -} from '@lifterlms/llms-e2e-test-utils'; - -import { - createNewPost, - publishPost, - pressKeyWithModifier, - visitAdminPage, -} from '@wordpress/e2e-test-utils'; - -const addSection = async function( title ) { - - await page.click( '#llms-new-section' ); - await page.waitForTimeout( 1000 ); - const selector = '#llms-sections li:last-child h2.llms-headline .llms-input'; - await fillField( selector, title ); - await page.$eval( selector, e => e.blur() ); - -} - -// const addLesson = async function( title, section_title ) { - -// if ( section_title ) { -// await clickElementByText( section_title, '#llms-sections > li h2.llms-headline .llms-input' ); -// } - -// await page.waitForTimout( 10000 ); - - - -// // await page.click( '#llms-new-lesson' ); -// await page.waitForTimout( 1000 ); - -// // await clickElementByText( 'New Lesson', '#llms-sections li.llms-lesson h3.llms-headline .llms-input' ); - -// // await pressKeyWithModifier( 'primary', 'a' ); -// // await page.keyboard.type( title ); -// // await page.$eval( selector, e => e.blur() ); - -// } - -const waitForSave = async function(){ - - await page.waitForSelector( '#llms-save-button[data-status="unsaved"]' ); - await page.waitForSelector( '#llms-save-button[data-status="saved"]' ); - -} - -let courseId = null; - -async function getCourseId() { - - if ( ! courseId ) { - - page.on( 'dialog', dialog => dialog.accept() ); - - await createNewPost( { - title: 'Test Course Builder', - postType: 'course', - } ); - - await publishPost(); - - courseId = await page.evaluate( () => wp.data.select( 'core/editor' ).getCurrentPostId() ); - - } - - return courseId; - -} - -describe( 'Builder', () => { - - beforeEach( async () => { - await getCourseId(); - } ); - - it ( 'should load the course builder from the WP editor metabox.', async () => { - - // Launch the builder. - await clickAndWait( '.llms-builder-launcher .llms-button-primary' ); - - expect( await page.$eval( '.llms-course-header h1.llms-headline .llms-input', el => el.textContent ) ).toBe( 'Test Course Builder' ); - - } ); - - it ( 'should create and save a new section.', async () => { - - const title = 'Test Section One'; - - await visitAdminPage( 'admin.php', 'page=llms-course-builder&course_id=' + courseId ); - - await addSection( title ); - - await waitForSave(); - - await page.reload(); - - expect( await page.$eval( '#llms-sections li:last-child h2.llms-headline .llms-input', el => el.textContent ) ).toBe( title ); - - } ); - - // it ( 'should create and save a new lesson.', async() => { - - // const title = 'Test New Lesson One'; - - // await visitAdminPage( 'admin.php', 'page=llms-course-builder&courseId=' + courseId ); - - // await addLesson( title, 'Test Section One' ); - - // } ); - -} ); diff --git a/tests/e2e/tests/checkout/coupon.test.js b/tests/e2e/tests/checkout/coupon.test.js deleted file mode 100644 index 2392eccfde..0000000000 --- a/tests/e2e/tests/checkout/coupon.test.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Test coupon-related actions on the checkout screen - * - * @since 3.39.0 - */ - -import { - click, - createAccessPlan, - createCoupon, - createCourse, - fillField, - logoutUser, -} from '@lifterlms/llms-e2e-test-utils'; - -let courseId = null, - coupon = null, - planUrl = null; - -/** - * Setup the test - * - * @since 3.39.0 - * - * @return {Void} - */ -async function setupTest() { - - if ( ! courseId ) { - courseId = await createCourse( 'Test Coupons' ); - } - - if ( ! planUrl ) { - planUrl = await createAccessPlan( { - postId: courseId, - price: 9.99, - } ); - - } - - if ( ! coupon ) { - coupon = await createCoupon( {} ); - } - - await logoutUser(); - -} - -/** - * Apply a coupon - * - * @since 3.39.0 - * - * @param {String} code Coupon code. - * @return {Void} - */ -async function applyCoupon( code ) { - - await page.goto( planUrl ); - - await click( '.llms-coupon-wrapper a[href="#llms-coupon-toggle"]' ); - - await page.waitForSelector( '#llms_coupon_code' ); - - await fillField( '#llms_coupon_code', code ); - - await click( '#llms-apply-coupon' ); - -} - -describe( 'Checkout/Coupons', () => { - - beforeEach( async () => { - await setupTest(); - } ); - - it ( 'should respond with an error for an unknown coupon', async () => { - - const codeNotFound = 'notfound'; - - await applyCoupon( codeNotFound ); - - await page.waitForSelector( '.llms-coupon-messages' ); - // Wait for animation. - await page.waitForTimeout( 500 ); - - expect( await page.$eval( '.llms-coupon-messages .llms-notice.llms-error li:first-child', el => el.textContent ) ).toBe( `Coupon code "${ codeNotFound }" not found.` ); - - } ); - - it ( 'should accept an existing coupon, save it to session data, and allow it to be removed', async () => { - - // Add a valid coupon. - await applyCoupon( coupon ); - await page.waitForSelector( '.llms-coupon-wrapper .llms-notice.llms-success' ); - expect( await page.$eval( '.llms-coupon-wrapper .llms-notice.llms-success', el => el.textContent ) ).toBe( `Coupon code "${ coupon }" has been applied to your order.` ); - - // Navigate away. - await page.goto( process.env.WP_BASE_URL ); - - // Return and it still found due to it being saved in session data. - await page.goto( planUrl ); - expect( await page.$eval( '.llms-coupon-wrapper .llms-notice.llms-success', el => el.textContent ) ).toBe( `Coupon code "${ coupon }" has been applied to your order.` ); - - // Remove it. - await click( '#llms-remove-coupon' ); - await page.waitForSelector( '.llms-coupon-wrapper a[href="#llms-coupon-toggle"]' ); - expect( await page.$eval( '.llms-coupon-wrapper a[href="#llms-coupon-toggle"]', el => el.textContent ) ).toBe( 'Click here to enter your code' ); - - } ); - -} ); diff --git a/tests/e2e/tests/engagements/certificates.test.js b/tests/e2e/tests/engagements/certificates.test.js deleted file mode 100644 index 2d8201bbaa..0000000000 --- a/tests/e2e/tests/engagements/certificates.test.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Test certificates - * - * @since 4.5.0 - * @since 4.15.0 Added hack to work around WP core bug on 5.6.1. - */ - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -import { - clickAndWait, - createCertificate, - fillField, - loginStudent, - logoutUser, - registerStudent, - toggleOpenRegistration, -} from '@lifterlms/llms-e2e-test-utils'; - -/** - * Reusable set of expectations to ensure a certificate looks right - * - * Intended to ensure the cert looks right on the frontend of the website - * - * @since 4.5.0 - * - * @param {String} name Students name. - * @param {String} title Certificate's title. - * @return {Void} - */ -async function certLooksRight( name, title = 'A Certificate!' ) { - expect( await page.$eval( '.llms-summary > h1', el => el.textContent ) ).toBe( title ); - expect( await page.$eval( '.llms-summary', el => el.textContent.includes( name ) ) ).toBe( true ); -} - -describe( 'Engagements/Certificates', () => { - - let certificateId, engagementId; - - beforeAll( async () => { - - await toggleOpenRegistration( true ); - - const posts = await createCertificate( { - title: 'A Certificate!', - engagement: 'user_registration', - } ); - certificateId = posts.certificateId; - engagementId = posts.engagementId; - - } ); - - - afterAll( async () => { - await toggleOpenRegistration( false ); - } ); - - describe( 'CRUD Template', () => { - - it ( 'should create a certificate', async () => { - - // Required due to WP core bug in 5.6.1 & later, see https://core.trac.wordpress.org/ticket/52440. - page.on( 'dialog', dialog => dialog.accept() ); - - await visitAdminPage( 'post.php', `post=${ certificateId }&action=edit` ); - await clickAndWait( '#sample-permalink a' ); - await certLooksRight( 'admin' ); - - } ); - - it ( 'should be able to view a student certificate from reporting screens', async () => { - - // Create a user who will earn the certificate. - const - first = 'Student', - last = 'WithACert', - { email } = await registerStudent( { first, last } ); - await logoutUser(); - - await visitAdminPage( 'users.php', `s=${ encodeURIComponent( email ) }` ); - - await page.goto( await page.$eval( '#the-list tr:first-child span.llms-reporting a', el => `${ el.href }&stab=certificates` ) ); - - const reportingUrl = await page.url(); - - // Navigate to the certificate page. - await page.goto( await page.$eval( '#llms-gb-table-certificates td.actions a', el => el.href ) ); - - await certLooksRight( 'A Student Who Has a Certificate' ); - - await page.goto( reportingUrl ); - // page.on( 'dialog', dialog => dialog.accept() ); // Uncomment when https://core.trac.wordpress.org/ticket/52440 is resolved. - await clickAndWait( '#llms_delete_cert' ); - - } ); - - } ); - - describe( 'Earn and View as a Student', () => { - - it ( 'should reward a certificate on user registration', async () => { - - const - first = 'Maude', - last = 'Lebowski', - user = await registerStudent( { first, last } ); - - // Certificate listed on the certs area of the dashboard. - const selector = '.llms-sd-section.llms-my-certificates li.llms-certificate-loop-item a.llms-certificate'; - expect( await page.$eval( `${ selector } h4.llms-certificate-title`, el => el.textContent ) ).toBe( 'A Certificate!' ); - - // Visit the certificate permalink. - await clickAndWait( selector ); - const certUrl = await page.url(); - - // Cert "looks" right. - await certLooksRight( 'Maude Lebowski' ); - - // Logged out user cannot view. - await logoutUser(); - await page.goto( certUrl ); - expect( await page.waitForSelector( 'body.error404' ) ).toBeTruthy(); - - // Student can enable sharing. - await loginStudent( user.email, user.pass ); - await page.goto( certUrl ); - - // Looks right to the student. - await certLooksRight( 'Maude Lebowski' ); - - // Enable sharing. - await clickAndWait( 'button[name="llms_enable_cert_sharing"]' ); - - // Logged out user can view. - await logoutUser(); - await page.goto( certUrl ); - await certLooksRight( 'Maude Lebowski' ); - await expect( await page.$( '#llms-print-certificate' ) ).toBeNull(); - - } ); - - } ); - -} ); diff --git a/tests/e2e/tests/page-restrictions/course.test.js b/tests/e2e/tests/page-restrictions/course.test.js deleted file mode 100644 index eb18e1591c..0000000000 --- a/tests/e2e/tests/page-restrictions/course.test.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Test restrictions when a sitewide membership is enabled. - * - * @since 4.3.1 - */ - -import { - clickAndWait, - createUser, - enrollStudent, - importCourse, - logoutUser, -} from '@lifterlms/llms-e2e-test-utils'; - -import { - createURL, - loginUser, -} from '@wordpress/e2e-test-utils'; - -describe( 'CourseRestrictions', () => { - - let course = {}, - lessons = []; - - beforeAll( async () => { - - await importCourse( 'import-with-restrictions.json' ); - await clickAndWait( '.llms-builder-launcher a.llms-button-primary' ); - - course = await page.evaluate( () => window.llms_builder.course ); - lessons = course.sections[0].lessons; - - } ); - - describe( 'Enrolled users', () => { - - beforeAll( async () => { - const { id, email, password } = await createUser(); - await enrollStudent( course.id, id ); - await logoutUser(); - await loginUser( email, password ); - } ); - - it ( 'should see enrolled user content on the course page', async () => { - - await page.goto( course.permalink ); - expect( await page.$eval( '.entry-content #enrolled-user-content', el => el.textContent ) ).toBe( 'Enrolled user content.' ); - - } ); - - it ( 'should be able to view a lesson with no restrictions', async () => { - - await page.goto( lessons[0].permalink ); // Lesson: "Regular". - - // On the right page. - expect( await page.$eval( '.entry-title', el => el.textContent ) ).toBe( 'Regular' ); - - // Mark complete is visible. - expect( await page.$eval( '#llms_mark_complete', el => el.textContent ) ).toBe( 'Mark Complete' ); - - } ); - - it ( 'should be redirected when accessing a lesson with unmet prerequisites', async () => { - - await page.goto( lessons[1].permalink ); // Lesson: "Has Prereq". - - // Redirected to the prerequisite lesson. - expect( await page.url() ).toBe( lessons[0].permalink ); - - // Shown an error message. - expect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'The lesson "Has Prereq" cannot be accessed until the required prerequisite "Regular" is completed.' ); - - - } ); - - it ( 'should be able to access lessons with prerequisites when the prerequisite is complete', async () => { - - await page.goto( lessons[0].permalink ); // Lesson: "Regular". - - await clickAndWait( '#llms_mark_complete' ); - - // Redirected to the next lesson (the one with the prereq). - expect( await page.url() ).toBe( lessons[1].permalink ); - - // On the right page. - expect( await page.$eval( '.entry-title', el => el.textContent ) ).toBe( 'Has Prereq' ); - - // Mark complete is visible. - expect( await page.$eval( '#llms_mark_complete', el => el.textContent ) ).toBe( 'Mark Complete' ); - - } ); - - it ( 'should be redirected when accessing a lesson that is not available because of a drip delay', async () => { - - await page.goto( lessons[2].permalink ); // Lesson: "Has Drip". - - // Redirected to the course. - expect( await page.url() ).toBe( course.permalink ); - - // Shown an error message. - expect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent.includes( 'The lesson "Has Drip" will be available on ' ) ) ).toBe( true ); - - } ); - - it ( 'should be able to view free lessons', async () => { - - await page.goto( lessons[3].permalink ); // Lesson: "Is Free". - expect( await page.$eval( '.entry-content #free-lesson-content', el => el.textContent ) ).toBe( 'Free lesson content.' ); - - } ); - - it ( 'should be able to access and take a quiz', async () => { - - await page.goto( lessons[4].permalink ); // Lesson: "Has Quiz" - - // On the right page. - expect( await page.$eval( '.entry-title', el => el.textContent ) ).toBe( 'Has Quiz' ); - - // Take quiz button is visible. - expect( await page.$eval( '#llms_start_quiz', el => el.textContent.trim() ) ).toBe( 'Take Quiz' ); - - await clickAndWait( '#llms_start_quiz' ); - - // On the quiz page. - expect( await page.url() ).toBe( lessons[4].quiz.permalink ); - - // Start button visible. - expect( await page.$eval( '#llms_start_quiz', el => el.textContent.trim() ) ).toBe( 'Start Quiz' ); - - } ); - - } ); - - describe( 'Non-enrolled users', () => { - - beforeAll( async () => { - await logoutUser(); - } ); - - it ( 'should see sales page content on the course page', async () => { - - await page.goto( course.permalink ); - expect( await page.$eval( '.entry-content #non-enrolled-user-content', el => el.textContent ) ).toBe( 'Non-enrolled user content.' ); - - } ); - - it ( 'should not be able to click syllabus links or view lesson URLs', async () => { - - await page.goto( course.permalink ); - expect( await page.$eval( '.llms-syllabus-wrapper .llms-lesson-preview a', el => el.href ) ).toBe( `${ course.permalink }#llms-lesson-locked` ); - - } ); - - it ( 'should be redirected to the course when accessing a lesson', async () => { - - await page.goto( lessons[0].permalink ); // Lesson: "Regular". - - // Redirected to the course. - expect( await page.url() ).toBe( course.permalink ); - - // Shown an error message. - expect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'You must enroll in this course to access course content.' ); - - } ); - - it ( 'should be able to view free lessons', async () => { - - await page.goto( lessons[3].permalink ); // Lesson: "Is Free". - expect( await page.$eval( '.entry-content #free-lesson-content', el => el.textContent ) ).toBe( 'Free lesson content.' ); - - } ); - - it ( 'should not be able to access quizzes', async () => { - - await page.goto( lessons[4].quiz.permalink ); - - // Redirected to dashboard. - expect( await page.url() ).toBe( createURL( '/dashboard/my-courses/' ) ); - - // Shown an error message. - expect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'You must be logged in to take quizzes.' ); - - } ); - - - } ); - - -} ); diff --git a/tests/e2e/tests/page-restrictions/sitewide-membership.test.js b/tests/e2e/tests/page-restrictions/sitewide-membership.test.js deleted file mode 100644 index 6d7b29515b..0000000000 --- a/tests/e2e/tests/page-restrictions/sitewide-membership.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Test restrictions when a sitewide membership is enabled. - * - * @since 4.3.1 - */ - -import { - clickAndWait, - createMembership, - setSelect2Option, - visitSettingsPage, -} from '@lifterlms/llms-e2e-test-utils'; - -import { - loginUser, - visitAdminPage -} from '@wordpress/e2e-test-utils'; - -describe( 'SitewideMembershipRestrictions', () => { - - // beforeAll( async () => { - // const membership_id = await createMembership( 'Sitewide Membership' ); - // await visitSettingsPage( { tab: 'memberships' } ); - // await setSelect2Option( '#lifterlms_membership_required', membership_id ); - // await clickAndWait( '.llms-save .llms-button-primary' ); - // } ); - - it ( 'should not allow logged out users to view the homepage', async () => { - } ); - -} ); diff --git a/tests/e2e/tests/settings/copy-prevention.test.js b/tests/e2e/tests/settings/copy-prevention.test.js deleted file mode 100644 index 260f726bda..0000000000 --- a/tests/e2e/tests/settings/copy-prevention.test.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Test the Setup Wizard - * - * @since 3.37.8 - * @since 3.37.14 Fix package references. - * @since 4.5.0 Use package functions. - * @since 4.12.0 Added registration test with a voucher. - * @since 5.0.0 Added tests for form field localization (country, state, etc...). - * @since 5.5.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - */ - -import { - clickAndWait, - highlightNode, - logoutUser, - loginStudent, - setCheckboxSetting, - visitPage, - visitSettingsPage, -} from '@lifterlms/llms-e2e-test-utils'; - -import { switchUserToAdmin } from '@wordpress/e2e-test-utils'; - -const context = browser.defaultBrowserContext(); -context.overridePermissions( process.env.WP_BASE_URL, [ 'clipboard-read' ] ); - -/** - * Watch for an event to run. - * - * @since 5.6.0 - * - * @param {string} eventName The event name. - * @return {void} - */ -async function watchForEvent( eventName ) { - return await page.evaluate( ( _eventName ) => { - document.addEventListener( _eventName, ( event ) => window.watchCopyPreventionEvents( { eventName: _eventName, event } ) ); - }, eventName ); -} - -describe( 'Setting/CopyPrevention', () => { - - let caughtEvents = []; - - beforeAll( async () => { - await visitSettingsPage(); - await setCheckboxSetting( '#lifterlms_content_protection', true ); - await page.exposeFunction( 'watchCopyPreventionEvents', ( event ) => { - caughtEvents.push( event ); - } ); - } ); - - afterAll( async () => { - await visitSettingsPage(); - await setCheckboxSetting( '#lifterlms_content_protection', false ); - await logoutUser(); - } ); - - beforeEach( async () => { - await visitPage( 'integrity-test' ); - } ); - - afterEach( () => { - caughtEvents = []; - } ); - - describe( 'AdminUser', () => { - - beforeAll( async() => { - await switchUserToAdmin(); - } ); - - it ( 'is allowed to copy content', async () => { - - watchForEvent( 'llms-copy-prevented' ); - expect( await highlightNode( 'h1.entry-title', true ) ).toBe( 'Integrity-Test' ); - expect( caughtEvents.length ).toStrictEqual( 0 ); - - } ); - - } ); - - describe( 'StudentUser', () => { - - beforeAll( async() => { - - await logoutUser(); - await loginStudent( 'validcreds@email.tld', 'password' ); - - } ); - - it ( 'is not allowed to copy content', async () => { - - watchForEvent( 'llms-copy-prevented' ); - expect( await highlightNode( 'h1.entry-title', true ) ).toBe( 'Copying is not allowed.' ); - expect( caughtEvents[0].eventName ).toBe( 'llms-copy-prevented' ); - - } ); - - } ); - - describe( 'LoggedOutUser', () => { - - beforeAll( async() => { - - await logoutUser(); - await visitPage( 'integrity-test' ); - - } ); - - it ( 'is not allowed to copy content', async () => { - - await visitPage( 'integrity-test' ); - watchForEvent( 'llms-copy-prevented' ); - expect( await highlightNode( 'h1.entry-title', true ) ).toBe( 'Copying is not allowed.' ); - expect( caughtEvents[0].eventName ).toBe( 'llms-copy-prevented' ); - - } ); - - } ); -} ); diff --git a/tests/e2e/tests/student/__snapshots__/open-registration.test.js.snap b/tests/e2e/tests/student/__snapshots__/open-registration.test.js.snap deleted file mode 100644 index 257fc90ed6..0000000000 --- a/tests/e2e/tests/student/__snapshots__/open-registration.test.js.snap +++ /dev/null @@ -1,175 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 1`] = `"Province*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 2`] = `"City*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 3`] = `"Postal code*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 4`] = `"Region*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 5`] = `"District*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 6`] = `"Postal code*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 7`] = `"State*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 8`] = `"City*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 9`] = `"ZIP code*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 10`] = `"Emirate*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 11`] = `"City*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country 12`] = `"Postal code*"`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country: China 1`] = ` -Object { - "AH": "Anhui", - "BJ": "Beijing", - "CQ": "Chongqing", - "FJ": "Fujian", - "GD": "Guangdong", - "GS": "Gansu", - "GX": "Guangxi Zhuang Autonomous Region", - "GZ": "Guizhou", - "HA": "Henan", - "HB": "Hubei", - "HE": "Hebei", - "HI": "Hainan", - "HK": "Hong Kong", - "HL": "Heilongjiang", - "HN": "Hunan", - "JL": "Jilin", - "JS": "Jiangsu", - "JX": "Jiangxi", - "LN": "Liaoning", - "MO": "Macau", - "NM": "Inner Mongolia", - "NX": "Ningxia Hui Autonomous Region", - "QH": "Qinghai", - "SC": "Sichuan", - "SD": "Shandong", - "SH": "Shanghai", - "SN": "Shaanxi", - "SX": "Shanxi", - "TW": "Taiwan Province, People's Republic of China", - "TW-KEE": "Keelung", - "XJ": "Xinjiang", - "XZ": "Tibet Autonomous Region", - "YN": "Yunnan", - "ZJ": "Zhejiang", -} -`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country: Peru 1`] = ` -Object { - "AMA": "Amazonas", - "ANC": "Áncash", - "APU": "Apurímac", - "ARE": "Arequipa", - "AYA": "Ayacucho", - "CAJ": "Cajamarca", - "CAL": "Callao", - "CUS": "Cusco", - "HUC": "Huanuco", - "HUV": "Huancavelica", - "ICA": "Ica", - "JUN": "Junín", - "LAL": "La Libertad", - "LAM": "Lambayeque", - "LIM": "Lima", - "MDD": "Madre de Dios", - "MOQ": "Moquegua", - "PAS": "Pasco", - "PIU": "Piura", - "PUN": "Puno", - "SAM": "San Martín", - "TAC": "Tacna", - "TUM": "Tumbes", - "UCA": "Ucayali", -} -`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country: Tokelau 1`] = ` -Object { - " ": " ", -} -`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country: United Arab Emirates 1`] = ` -Object { - "AJ": "Ajman Emirate", - "AZ": "Abu Dhabi Emirate", - "DU": "Dubai", - "FU": "Fujairah", - "RK": "Ras al-Khaimah", - "SH": "Sharjah Emirate", - "UQ": "Umm al-Quwain", -} -`; - -exports[`OpenRegistration Localization should localize city, state, and postcode fields when changing the selected country: United States 1`] = ` -Object { - "AK": "Alaska", - "AL": "Alabama", - "AR": "Arkansas", - "AS": "American Samoa", - "AZ": "Arizona", - "CA": "California", - "CO": "Colorado", - "CT": "Connecticut", - "DC": "District of Columbia", - "DE": "Delaware", - "FL": "Florida", - "GA": "Georgia", - "GU": "Guam", - "HI": "Hawaii", - "IA": "Iowa", - "ID": "Idaho", - "IL": "Illinois", - "IN": "Indiana", - "KS": "Kansas", - "KY": "Kentucky", - "LA": "Louisiana", - "MA": "Massachusetts", - "MD": "Maryland", - "ME": "Maine", - "MI": "Michigan", - "MN": "Minnesota", - "MO": "Missouri", - "MP": "Northern Mariana Islands", - "MS": "Mississippi", - "MT": "Montana", - "NC": "North Carolina", - "ND": "North Dakota", - "NE": "Nebraska", - "NH": "New Hampshire", - "NJ": "New Jersey", - "NM": "New Mexico", - "NV": "Nevada", - "NY": "New York", - "OH": "Ohio", - "OK": "Oklahoma", - "OR": "Oregon", - "PA": "Pennsylvania", - "PR": "Puerto Rico", - "RI": "Rhode Island", - "SC": "South Carolina", - "SD": "South Dakota", - "TN": "Tennessee", - "TX": "Texas", - "UM": "United States Minor Outlying Islands", - "UT": "Utah", - "VA": "Virginia", - "VI": "United States Virgin Islands", - "VT": "Vermont", - "WA": "Washington", - "WI": "Wisconsin", - "WV": "West Virginia", - "WY": "Wyoming", -} -`; - -exports[`OpenRegistration Registration should register a new user with a voucher. 1`] = `" <h4 class=\\"llms-notification-title\\">Course enrollment success!</h4> <div class=\\"llms-notification-body\\"><p>Congratulations! You enrolled in LifterLMS Quickstart Course</p> </div> "`; diff --git a/tests/e2e/tests/student/__snapshots__/voucher.test.js.snap b/tests/e2e/tests/student/__snapshots__/voucher.test.js.snap deleted file mode 100644 index 43504feb6f..0000000000 --- a/tests/e2e/tests/student/__snapshots__/voucher.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StudentDashboard/RedeemVoucher Should display an error for an invalid voucher 1`] = ` -" - Voucher code \\"fakecode\\" could not be found. - " -`; - -exports[`StudentDashboard/RedeemVoucher Should redeem a valid voucher 1`] = `"Voucher redeemed successfully!"`; - -exports[`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> "`; diff --git a/tests/e2e/tests/student/login.test.js b/tests/e2e/tests/student/login.test.js deleted file mode 100644 index 024ca110eb..0000000000 --- a/tests/e2e/tests/student/login.test.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Test the Setup Wizard - * - * @since 3.37.8 - * @since 3.37.14 Fix package references. - * @since 5.5.0 Use user created via setup-e2e.sh in favor of `createUser()`. - */ - -import { - loginStudent, - logoutUser, - visitPage, -} from '@lifterlms/llms-e2e-test-utils'; - -import { - loginUser, - visitAdminPage -} from '@wordpress/e2e-test-utils'; - -describe( 'StudentDashboardLogin', () => { - - afterEach( async () => { - await logoutUser(); - } ); - - it ( 'should not allow a user to login if they are already logged in.', async () => { - - await loginUser(); - await visitPage( 'dashboard' ); - await expect( await page.$( '.llms-new-person-login-wrapper > h4.llms-form-heading' ) ).toBeNull(); - - } ); - - it ( 'should display an error message when invalid credentials are used.', async () => { - - await loginStudent( 'fake@fake.tld', 'fake' ); - await 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.' ); - - } ); - - it ( 'should allow a user with valid credentials to login.', async () => { - - await loginStudent( 'validcreds@email.tld', 'password' ); - expect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' ); - - } ); - -} ); diff --git a/tests/e2e/tests/student/open-registration.test.js b/tests/e2e/tests/student/open-registration.test.js deleted file mode 100644 index 537566508d..0000000000 --- a/tests/e2e/tests/student/open-registration.test.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Test the Setup Wizard - * - * @since 3.37.8 - * @since 3.37.14 Fix package references. - * @since 4.5.0 Use package functions. - * @since 4.12.0 Added registration test with a voucher. - * @since 5.0.0 Added tests for form field localization (country, state, etc...). - * @since 5.5.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`. - */ - -import { - clickAndWait, - createVoucher, - fillField, - logoutUser, - registerStudent, - select2Select, - toggleOpenRegistration, - visitPage, -} from '@lifterlms/llms-e2e-test-utils'; - -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -let openRegStatus = null; - -/** - * Toggles the open registration setting on or off - * - * @since 3.37.8 - * @since 4.5.0 Use toggleOpenRegistration function from utils pacakage. - * - * @param {Boolean} status Whether to toggle on (`true`) or off (`false`). - * @return {void} - */ -const toggleOpenReg = async function( status ) { - - if ( openRegStatus === status ) { - return; - } - await toggleOpenRegistration( status ); - -} - -describe( 'OpenRegistration', () => { - - afterEach( async () => { - await logoutUser(); - } ); - - describe( 'Registration', () => { - - it ( 'should not allow registration because user is already logged in.', async () => { - - await toggleOpenReg( true ); - await visitPage( 'dashboard' ); - await expect( await page.$( '.llms-new-person-form-wrapper > h4.llms-form-heading' ) ).toBeNull(); - - } ); - - it ( 'should allow registration.', async () => { - - await toggleOpenReg( true ); - await logoutUser(); - await visitPage( 'dashboard' ); - expect( await page.$eval( '.llms-new-person-form-wrapper > h4.llms-form-heading', el => el.textContent ) ).toBe( 'Register' ); - - } ); - - it ( 'should register a new user.', async () => { - - await toggleOpenReg( true ); - await registerStudent(); - expect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' ); - - } ); - - it ( 'should register a new user with a voucher.', async() => { - - await toggleOpenReg( true ); - const codes = await createVoucher( { codes: 1, uses: 1 } ); - await registerStudent( { voucher: codes[0] } ); - - expect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' ); - - await page.waitForSelector( '.llms-notification .llms-notification-main' ); - expect( await page.$eval( '.llms-notification .llms-notification-main', el => el.innerHTML ) ).toMatchSnapshot(); - - } ); - - it ( 'should not allow registration because open registration is disabled.', async () => { - - await toggleOpenReg( false ); - await logoutUser(); - await visitPage( 'dashboard' ); - await expect( await page.$( '.llms-new-person-form-wrapper > h4.llms-form-heading' ) ).toBeNull(); - - } ); - - } ); - - describe( 'Localization', () => { - - it ( 'should localize city, state, and postcode fields when changing the selected country', async () => { - - const selectCountry = async ( country ) => { - await select2Select( '#llms_billing_country', country ); - }; - - const getStatesList = async () => { - const list = await page.$$eval( '#llms_billing_state option', els => - els.map( ( { value, textContent } ) => ( [ value, textContent ] ) ) ); - - return Object.fromEntries( list ); - }; - - await toggleOpenReg( true ); - await logoutUser(); - await visitPage( 'dashboard' ); - - // China. - await selectCountry( 'China' ); - expect( await page.$eval( 'label[for="llms_billing_state"]', el => el.textContent ) ).toMatchSnapshot(); - expect ( await getStatesList() ).toMatchSnapshot( 'China' ); - expect( await page.$eval( 'label[for="llms_billing_city"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( 'label[for="llms_billing_zip"]', el => el.textContent ) ).toMatchSnapshot(); - - await page.waitForTimeout( 1000 ); - - // Peru changes name of the State & City fields. - await selectCountry( 'Peru' ); - expect( await page.$eval( 'label[for="llms_billing_state"]', el => el.textContent ) ).toMatchSnapshot(); - expect ( await getStatesList() ).toMatchSnapshot( 'Peru' ); - expect( await page.$eval( 'label[for="llms_billing_city"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( 'label[for="llms_billing_zip"]', el => el.textContent ) ).toMatchSnapshot(); - - await page.waitForTimeout( 1000 ); - - // United States. - await selectCountry( 'United States' ); - expect ( await getStatesList() ).toMatchSnapshot( 'United States' ); - expect( await page.$eval( 'label[for="llms_billing_state"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( 'label[for="llms_billing_city"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( 'label[for="llms_billing_zip"]', el => el.textContent ) ).toMatchSnapshot(); - - await page.waitForTimeout( 1000 ); - - // UAB has no postal code or city. - await selectCountry( 'United Arab Emirates' ); - expect ( await getStatesList() ).toMatchSnapshot( 'United Arab Emirates' ); - expect( await page.$eval( 'label[for="llms_billing_state"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( '#llms_billing_city', el => el.disabled ) ).toBe( true ); - expect( await page.$eval( '#llms_billing_zip', el => el.disabled ) ).toBe( true ); - - await page.waitForTimeout( 1000 ); - - // Tokelau has no states. - await selectCountry( 'Tokelau' ); - expect ( await getStatesList() ).toMatchSnapshot( 'Tokelau' ); - expect( await page.$eval( '#llms_billing_state', el => el.disabled ) ).toBe( true ); - expect( await page.$eval( 'label[for="llms_billing_city"]', el => el.textContent ) ).toMatchSnapshot(); - expect( await page.$eval( 'label[for="llms_billing_zip"]', el => el.textContent ) ).toMatchSnapshot(); - - } ); - - } ); - -} ); diff --git a/tests/e2e/tests/student/voucher.test.js b/tests/e2e/tests/student/voucher.test.js deleted file mode 100644 index c9d4cdf52f..0000000000 --- a/tests/e2e/tests/student/voucher.test.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Test voucher redemption on the student dashboard - * - * @since 4.12.0 - */ - -import { - clickAndWait, - createVoucher, - fillField, - loginStudent, - logoutUser, - visitPage, -} from '@lifterlms/llms-e2e-test-utils'; - -describe( 'StudentDashboard/RedeemVoucher', () => { - - afterEach( async () => { - await logoutUser(); - } ); - - it ( 'Should redeem a valid voucher', async () => { - - // Setup. - const codes = await createVoucher( { codes: 1, uses: 1 } ); - - await logoutUser(); - - // Use the voucher. - await loginStudent( 'voucher@email.tld', 'password' ); - await visitPage( 'dashboard/redeem-voucher' ); - await fillField( '#llms-voucher-code', codes[0] ); - await clickAndWait( '#llms-redeem-voucher-submit' ); - - // Success. - expect( await page.$eval( '.llms-notice.llms-success', el => el.textContent ) ).toMatchSnapshot(); - - await page.waitForSelector( '.llms-notification .llms-notification-main' ); - expect( await page.$eval( '.llms-notification .llms-notification-main', el => el.innerHTML ) ).toMatchSnapshot(); - - } ); - - it ( 'Should display an error for an invalid voucher', async() => { - - await logoutUser(); - - await loginStudent( 'voucher@email.tld', 'password' ); - await visitPage( 'dashboard/redeem-voucher' ); - await fillField( '#llms-voucher-code', 'fakecode' ); - await clickAndWait( '#llms-redeem-voucher-submit' ); - - // Error message. - expect( await page.$eval( '.llms-notice.llms-error', el => el.textContent ) ).toMatchSnapshot(); - - } ); - -} ); diff --git a/tests/e2e/tests/view-manager/__snapshots__/view-manager.test.js.snap b/tests/e2e/tests/view-manager/__snapshots__/view-manager.test.js.snap deleted file mode 100644 index bddc413548..0000000000 --- a/tests/e2e/tests/view-manager/__snapshots__/view-manager.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`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>"`; diff --git a/tests/e2e/tests/view-manager/view-manager.test.js b/tests/e2e/tests/view-manager/view-manager.test.js deleted file mode 100644 index 48009371ae..0000000000 --- a/tests/e2e/tests/view-manager/view-manager.test.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Test the LifterLMS View Manager - * - * @since 4.16.0 - */ - -import { - clickAndWait, - createAccessPlan, - createCourse, - logoutUser, - toggleOpenRegistration, - visitPage, -} from '@lifterlms/llms-e2e-test-utils'; - -import { - visitAdminPage -} from '@wordpress/e2e-test-utils'; - - -/** - * Select a view from the view manager menu in the WP Admin bar. - * - * @since 4.16.0 - * - * @param {String} view View name to select. Accepts "self", "visitor", or "student". - * @return {Void} - */ -async function selectView( view ) { - - const - topLevelSelector = '#wp-admin-bar-llms-view-as-menu', - viewSelector = `#wp-admin-bar-llms-view-as--${ view }`; - - await page.waitForSelector( topLevelSelector ); - await page.hover( topLevelSelector ); - - await page.waitForSelector( viewSelector ); - await clickAndWait( `${ viewSelector } a.ab-item` ); - -} - -describe( 'ViewManager', () => { - - beforeAll( async () => { - // Ensure we're a logged in admin that can use the view manager. - await visitAdminPage( '/' ); - } ); - - afterAll( async () => { - await logoutUser(); - } ); - - describe( 'Dashboard', () => { - - beforeAll( async () => { - await toggleOpenRegistration( true ); - } ); - afterAll( async () => { - await toggleOpenRegistration( false ); - } ); - - beforeEach( async () => { - await visitPage( 'dashboard' ); - } ); - - it ( 'should show forms when viewing as a visitor.', async () => { - - await selectView( 'visitor' ); - - // Login and registration forms should exist. - await page.waitForSelector( '.llms-person-login-form-wrapper' ); - await page.waitForSelector( '.llms-new-person-form-wrapper' ); - - // Dashboard header should not exist. - expect( await page.$( '.llms-sd-header' ) ).toBeNull(); - - } ); - - it ( 'should show the dashboard when viewing as a student.', async () => { - - await selectView( 'student' ); - - // Login and registration forms should not exist. - expect( await page.$( '.llms-person-login-form-wrapper' ) ).toBeNull(); - expect( await page.$( '.llms-new-person-form-wrapper' ) ).toBeNull(); - - // Dashboard header should exist. - await page.waitForSelector( '.llms-sd-header' ); - - } ); - - } ); - - describe( 'Checkout', () => { - - beforeAll( async () => { - - const - courseId = await createCourse( 'View Manager Test' ), - planUrl = await createAccessPlan( { - postId: courseId, - price: 5.00, - } ); - - await page.goto( planUrl ); - - } ); - - it ( 'should show the checkout form when viewing as a visitor.', async () => { - - await selectView( 'visitor' ); - - // Should show the checkout form. - await page.waitForSelector( '#llms-product-purchase-form' ); - - } ); - - it ( 'should show an already enrolled notice when viewing as a student.', async () => { - - await selectView( 'student' ); - - // Should show a notice. - expect( await page.$eval( '.llms-checkout-wrapper .llms-notice', el => el.innerHTML ) ).toMatchSnapshot(); - - // Should not show the checkout form. - expect( await page.$( '#llms-product-purchase-form' ) ).toBeNull(); - - } ); - - } ); - -} ); diff --git a/tests/phpunit/README.md b/tests/phpunit/README.md deleted file mode 100644 index 6831a214a7..0000000000 --- a/tests/phpunit/README.md +++ /dev/null @@ -1,39 +0,0 @@ -LifterLMS Tests -=============== - -## Running Tests Locally - -To 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. - -### Installing - -1. Install all development dependencies via `composer install` -2. Install the testing database and environment: `composer run-script tests-install` - -### Running Tests - -+ Run tests: `composer run-script tests-run` -+ Run tests by group `composer run-script tests-run -- --group LLMS_Post_Model` -+ Run a specific tests `composer run-script tests-run -- --filter test_my_test_method` -+ Run tests and generate code coverage in HTML format: `composer run-script tests-run -- --coverage-html tmp/coverage` -+ Run tests and generate text code coverage: `composer run-script tests-run -- --coverage-text` - -## Automated Testing - -Tests are run automatically on commits and pull requests via [CircleCI](https://circleci.com/gh/gocodebox/lifterlms/tree/master). - -## Code Coverage - -Code coverage is available on [Code Climate](https://codeclimate.com/github/gocodebox/lifterlms/code?sort=-test_coverage) and updated automatically after each CircleCI build. - -## Writing Tests - -+ 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. -+ Each test method should cover a single method or function with one or more assertions -+ A single method or function can have multiple associated test methods if it's a large or complex method -+ Use coverage reports to examine which lines your tests are covering and aim for 100% coverage -+ In addition to covering each line of a method/function, make sure to test common input and edge cases. -+ Remember that only methods prefixed with test will be run so use helper methods liberally to keep test methods small and reduce code duplication. -+ 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. -+ 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 -+ 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. diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php deleted file mode 100644 index 1cd7e2fcc7..0000000000 --- a/tests/phpunit/bootstrap.php +++ /dev/null @@ -1,117 +0,0 @@ -<?php -/** - * LifterLMS Add-On Testing Bootstrap - * - * @package LifterLMS/Tests - * - * @since 3.3.1 - * @since 3.28.0 Unknown - * @since 3.37.8 Added class variable to access the tests assets directory. - */ - -require_once './vendor/lifterlms/lifterlms-tests/bootstrap.php'; - -class LLMS_Unit_Tests_Bootstrap extends LLMS_Tests_Bootstrap { - - /** - * __FILE__ reference, should be defined in the extending class - * - * @var [type] - */ - public $file = __FILE__; - - /** - * Name of the testing suite - * - * @var string - */ - public $suite_name = 'LifterLMS'; - - /** - * Main PHP File for the plugin - * - * @var string - */ - public $plugin_main = 'lifterlms.php'; - - /** - * Location of testing assets. - * - * @var string - */ - public $assets_dir = ''; - - /** - * Determines if the LifterLMS core should be loaded - * - * @var bool - */ - public $use_core = false; - - /** - * Install the plugin - * - * @return void - * @since 3.28.0 - * @version 3.28.0 - */ - public function install() { - - parent::install(); - - // install LLMS - LLMS_Install::install(); - - // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374 - if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) { - $GLOBALS['wp_roles']->reinit(); - } else { - $GLOBALS['wp_roles'] = null; - wp_roles(); - } - - } - - - /** - * Load the plugin - * - * @since 3.28.0 - * @since 3.37.8 Use $this->assets_dir. - * - * @return void - */ - public function load() { - - // Assets are shared between phpunit and e2e tests. - $this->assets_dir = dirname( $this->tests_dir ) . '/assets/'; - - // override this constant otherwise a bunch of includes will fail when running tests - // define( 'LLMS_PLUGIN_DIR', trailingslashit( $this->plugin_dir ) ); - - parent::load(); - - } - - /** - * Uninstall the plugin. - * - * @return void - * @since 3.28.0 - * @version 3.28.0 - */ - public function uninstall() { - - parent::uninstall(); - - // Clean existing install first. - define( 'LLMS_REMOVE_ALL_DATA', true ); - include( $this->plugin_dir . '/uninstall.php' ); - - } - -} - -global $lifterlms_tests; -$lifterlms_tests = new LLMS_Unit_Tests_Bootstrap(); -return $lifterlms_tests; diff --git a/tests/phpunit/framework/class-llms-admin-tool-test-case.php b/tests/phpunit/framework/class-llms-admin-tool-test-case.php deleted file mode 100644 index 40319d8dd2..0000000000 --- a/tests/phpunit/framework/class-llms-admin-tool-test-case.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/** - * Admin Tool base test case - * - * @package LifterLMS/Tests/Framework - * - * @since 5.3.0 - */ - -require_once 'class-llms-unit-test-case.php'; - -class LLMS_Admin_Tool_Test_Case extends LLMS_UnitTestCase { - - /** - * Name of the class being tested. - * - * This must be added to extending classes. - * - * @var sting - */ - // const CLASS_NAME = 'LLMS_Admin_Tool_Class_Name'; - - /** - * Setup before class - * - * Include abstract required classes. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - // Abstract tool. - require_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-admin-tool.php'; - - // Include the tool itself. - $filename = 'class-' . str_replace( '_', '-', strtolower( static::CLASS_NAME ) ) . '.php'; - require_once LLMS_PLUGIN_DIR . 'includes/admin/tools/' . $filename; - - } - - /** - * Setup test case - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $classname = static::CLASS_NAME; - $this->main = new $classname(); - - } - - /** - * Test get_description() - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_description() { - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_description' ); - $this->assertTrue( ! empty( $res ) ); - $this->assertTrue( is_string( $res ) ); - - } - - /** - * Test get_label() - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_label() { - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_label' ); - $this->assertTrue( ! empty( $res ) ); - $this->assertTrue( is_string( $res ) ); - - } - - /** - * Test get_text() - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_text() { - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_text' ); - $this->assertTrue( ! empty( $res ) ); - $this->assertTrue( is_string( $res ) ); - - } - -} diff --git a/tests/phpunit/framework/class-llms-notification-test-case.php b/tests/phpunit/framework/class-llms-notification-test-case.php deleted file mode 100644 index eef0f1c666..0000000000 --- a/tests/phpunit/framework/class-llms-notification-test-case.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Unit Test Case with tests and utilities specific to testing LifterLMS Notification Classes - * @since 3.8.0 - * @version 3.8.0 - */ - -require_once 'class-llms-unit-test-case.php'; - -class LLMS_NotificationTestCase extends LLMS_UnitTestCase { - - public function test_is_registered() { - - $main = LLMS()->notifications(); - - $controller = $main->get_controller( $this->notification_id ); - $this->assertTrue( is_a( $controller, 'LLMS_Abstract_Notification_Controller' ) ); - $this->assertEquals( $this->notification_id, $controller->id ); - - // $view = $main->get_view( $this->notification_id ); - // $this->assertTrue( is_a( $view, 'LLMS_Abstract_Notification_View' ) ); - // $this->assertEquals( $this->notification_id, $view->trigger_id ); - - } - -} diff --git a/tests/phpunit/framework/class-llms-payment-gateway-mock.php b/tests/phpunit/framework/class-llms-payment-gateway-mock.php deleted file mode 100644 index 2e804a1926..0000000000 --- a/tests/phpunit/framework/class-llms-payment-gateway-mock.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Mock payment gateway for testing. - * - * @since 3.37.6 - */ -class LLMS_Payment_Gateway_Mock extends LLMS_Payment_Gateway { - - /** - * Constructor - * - * @since 3.37.6 - * - * @return void - */ - public function __construct() { - - $this->id = 'mock'; - $this->admin_description = __( 'Mock payment gateway used for unit testing', 'lifterlms' ); - $this->admin_title = __( 'Mock', 'lifterlms' ); - $this->title = __( 'Mock', 'lifterlms' ); - $this->description = __( 'Make mock payments', 'lifterlms' ); - - $this->supports = array( - 'checkout_fields' => false, - 'refunds' => true, - 'single_payments' => true, - 'recurring_payments' => true, - 'test_mode' => false, - ); - - } - - /** - * Handle a Pending Order - * - * @since 3.37.6 - * - * @param LLMS_Order $order Order object. - * @param LLMS_AccessPlan $plan Access plan object. - * @param LLMS_Student $person Student object. - * @param false|LLMS_Coupon $coupon Coupon object, or false if none is being used. - * @return void - */ - public function handle_pending_order( $order, $plan, $person, $coupon = false ) { - - $payment_type = 'single'; - - if ( $order->is_recurring() ) { - $payment_type = $order->has_trial() ? 'trial' : 'recurring'; - } - - $order->record_transaction( - array( - 'amount' => $order->get_initial_price( array(), 'float' ), - 'source_description' => 'Mock Payment', - 'transaction_id' => uniqid( 'mock-' ), - 'status' => 'llms-txn-succeeded', - 'payment_gateway' => $this->id, - 'payment_type' => $payment_type, - ) - ); - - } - - /** - * Called by scheduled actions to charge an order for a scheduled recurring transaction - * - * This function must be defined by gateways which support recurring transactions - * - * @since 3.37.6 - * - * @param LLMS_Order $order Instance LLMS_Order for the order being processed. - * @return void - */ - public function handle_recurring_transaction( $order ) { - - $order->record_transaction( - array( - 'amount' => $order->get_price( 'total', array(), 'float' ), - 'source_description' => 'Mock Payment', - 'transaction_id' => uniqid( 'mock-' ), - 'status' => 'llms-txn-succeeded', - 'payment_gateway' => $this->id, - 'payment_type' => 'recurring', - ) - ); - - } - - /** - * Determine if the gateway is enabled according to admin settings checkbox. - * - * The mock gateway is always enabled. - * - * @since 3.37.6 - * - * @return boolean - */ - public function is_enabled() { - return true; - } - -} diff --git a/tests/phpunit/framework/class-llms-post-model-unit-test-case.php b/tests/phpunit/framework/class-llms-post-model-unit-test-case.php deleted file mode 100644 index e4817f2a0c..0000000000 --- a/tests/phpunit/framework/class-llms-post-model-unit-test-case.php +++ /dev/null @@ -1,341 +0,0 @@ -<?php -/** - * Unit Test Case with tests and utilities specific to testing classes - * which extend the LLMS_Post_Model - * - * @since 3.4.0 - * @since 3.34.0 Add tests for new `set_bulk()` method and other recently added properties. - */ - -require_once 'class-llms-unit-test-case.php'; - -class LLMS_PostModelUnitTestCase extends LLMS_UnitTestCase { - - /** - * class name for the model being tested by the class - * @var string - */ - protected $class_name = ''; - - /** - * db post type of the model being tested - * @var string - */ - protected $post_type = ''; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array - * - * @since 3.4.0 - * @since 4.5.0 Use unit test utils to retrieve `properties` array automatically. - * - * @return array - */ - protected function get_properties() { - return LLMS_Unit_Test_Util::get_private_property_value( new $this->class_name( 'new' ), 'properties' ); - } - - /** - * Get data to fill a create post with - * This is used by test_getters_setters - * @return array - * @since 3.4.0 - * @version 3.4.0 - */ - protected function get_data() { - return array(); - } - - /* - /$$ /$$ /$$ - | $$ |__/| $$ - /$$ /$$ /$$$$$$ /$$| $$ /$$$$$$$ - | $$ | $$|_ $$_/ | $$| $$ /$$_____/ - | $$ | $$ | $$ | $$| $$| $$$$$$ - | $$ | $$ | $$ /$$| $$| $$ \____ $$ - | $$$$$$/ | $$$$/| $$| $$ /$$$$$$$/ - \______/ \___/ |__/|__/|_______/ - */ - - /** - * Will hold an instance of the model being tested by the class - * @var obj - */ - protected $obj = null; - - - /** - * Create a post that can be tested - * @param string|array $args string for post title or array of arguments to use when creating the post - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - protected function create( $args = 'test title' ) { - - $this->obj = new $this->class_name( 'new', $args ); - - } - - /* - /$$ /$$ - | $$ | $$ - /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$ - |_ $$_/ /$$__ $$ /$$_____/|_ $$_/ /$$_____/ - | $$ | $$$$$$$$| $$$$$$ | $$ | $$$$$$ - | $$ /$$| $$_____/ \____ $$ | $$ /$$\____ $$ - | $$$$/| $$$$$$$ /$$$$$$$/ | $$$$//$$$$$$$/ - \___/ \_______/|_______/ \___/ |_______/ - */ - - /** - * Test creation of the model - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - public function test_create_model() { - - $this->create( 'test title' ); - - $id = $this->obj->get( 'id' ); - - $test = llms_get_post( $id ); - - $this->assertEquals( $id, $test->get( 'id' ) ); - $this->assertEquals( $this->post_type, $test->get( 'type' ) ); - $this->assertEquals( 'test title', $test->get( 'title' ) ); - - } - - /** - * Test getters and setters - * - * @return void - * @since 3.4.0 - * @version 3.28.0 - */ - public function test_getters_setters() { - - $this->create( 'test title' ); - $props = $this->get_properties(); - $data = $this->get_data(); - - if ( ! $data ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - foreach ( $props as $prop => $type ) { - - // set should return true - $this->assertTrue( $this->obj->set( $prop, $data[ $prop ] ) ); - - // make sure gotten value equals set val - $this->assertEquals( $data[ $prop ], $this->obj->get( $prop ) ); - - // check type - switch ( $type ) { - - case 'absint': - // should be numeric - $this->assertTrue( is_numeric( $this->obj->get( $prop ) ) ); - // strings should return 0 - $this->obj->set( $prop, 'string' ); - $this->assertEquals( 0, $this->obj->get( $prop ) ); - // floats should drop the decimal - $this->obj->set( $prop, 12.3 ); - $this->assertEquals( 12, $this->obj->get( $prop ) ); - // negative should return positive - $this->obj->set( $prop, -45 ); - $this->assertEquals( 45, $this->obj->get( $prop ) ); - // numeric string should return int - $this->obj->set( $prop, '6' ); - $this->assertEquals( '6', $this->obj->get( $prop ) ); - break; - - case 'array': - // should be an array - $this->assertTrue( is_array( $this->obj->get( $prop ) ) ); - // strings should return an array with the string as the first item in the array - $this->obj->set( $prop, 'string' ); - $this->assertEquals( array( 'string' ), $this->obj->get( $prop ) ); - break; - - case 'float': - // should be a float - $this->assertTrue( is_float( $this->obj->get( $prop ) ) ); - // string should return 0 - $this->obj->set( $prop, 'string' ); - $this->assertEquals( 0, $this->obj->get( $prop ) ); - // decimals shouldn't be lost - $this->obj->set( $prop, 123.456 ); - $this->assertEquals( 123.456, $this->obj->get( $prop ) ); - // whole numbers should still be whole numbers - $this->obj->set( $prop, 789 ); - $this->assertEquals( 789, $this->obj->get( $prop ) ); - // check super big numbers - $this->obj->set( $prop, 1234567.89 ); - $this->assertEquals( 1234567.89, $this->obj->get( $prop ) ); - break; - - case 'text': - $this->assertTrue( is_string( $this->obj->get( $prop ) ) ); - break; - - case 'yesno': - // yes returns yes - $this->obj->set( $prop, 'yes' ); - $this->assertEquals( 'yes', $this->obj->get( $prop ) ); - // no returns no - $this->obj->set( $prop, 'no' ); - $this->assertEquals( 'no', $this->obj->get( $prop ) ); - // anything else returns no - $this->obj->set( $prop, 'string' ); - $this->assertEquals( 'no', $this->obj->get( $prop ) ); - $this->obj->set( $prop, '' ); - $this->assertEquals( 'no', $this->obj->get( $prop ) ); - $this->obj->set( $prop, 123456 ); - $this->assertEquals( 'no', $this->obj->get( $prop ) ); - break; - - } - - } - } - - /** - * Test creation date and status relationship on updating. - * - * @since 3.34.0 - * - * @return void - */ - public function test_date_status_relationship_update() { - - if ( ! $this->get_data() ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - // Check we can update drafts creation date. - $this->create( 'test title date status relationship' ); - - // Check that when setting the creation date to the future, the post status changes accordingly. - $this->obj->set( 'status', 'publish' ); - $this->obj->set( 'date_gmt', date( 'Y-m-d H:i:s', strtotime( '+1 year', current_time( 'timestamp' ) ) ) ); - $this->assertEquals( 'future', $this->obj->get( 'status' ) ); - - } - - /** - * Test edit_date post proerty. - * - * @since 3.34.0 - * - * @return void - */ - public function test_edit_date() { - - if ( ! $this->get_data() ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - // Check we can update drafts creation date. - $this->create( 'test title draft' ); - - // Makes sense only for drafts. - if ( 'draft' !== $this->obj->get( 'status' ) ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - $new_date = date( 'Y-m-d H:i:s', strtotime( '-1 year', current_time( 'timestamp' ) ) ); - $this->obj->set_bulk( array( - 'date_gmt' => $new_date, - 'edit_date' => true, - ) ); - $this->assertEquals( $new_date, $this->obj->get( 'date_gmt' ) ); - - // Check we cannot update drafts creation dates without passing edit_date. - $this->create( 'test title draft two' ); - - $this->obj->set_bulk( array( - 'date_gmt' => $new_date, - ) ); - $this->assertNotEquals( $new_date, $this->obj->get( 'date_gmt' ) ); - $this->assertEquals( '0000-00-00 00:00:00', $this->obj->get( 'date_gmt' ) ); - - } - - - /** - * Test set_bulk() - * - * @since 3.34.0 - * @return void - */ - public function test_set_bulk() { - - $this->create( 'another creative test title' ); - $props = $this->get_properties(); - $data = $this->get_data(); - - if ( ! $data ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - // update should return true - $this->assertTrue( $this->obj->set_bulk( $data ) ); - - // Check each property has been set as expected. - foreach ( $props as $prop => $type ) { - // make sure gotten value equals set val - $this->assertEquals( $data[ $prop ], $this->obj->get( $prop ) ); - } - - // update should return false, the DB values are the same. - $this->assertFalse( $this->obj->set_bulk( $data ) ); - - } - - /** - * Test set_bulk() when passing $wp_error param as true. - * - * @since 3.34.0 - * @return void - */ - public function test_set_bulk_wp_error() { - - $this->create( 'a creative test title take one' ); - $props = $this->get_properties(); - $data = $this->get_data(); - - if ( ! $data ) { - $this->markTestSkipped( 'No properties to test.' ); - } - - // update should return true - $this->assertTrue( $this->obj->set_bulk( $data, $wp_error = true ) ); - - // Let's add some post data - $data['content'] = 'Special creative content'; - - // We're updating an llms post with exactly the same set of metas - // this will produce a wp_error object with the error code 'invalid_meta'. - $result = $this->obj->set_bulk( $data, $wp_error = true ); - $this->assertWPError( $result ); - $this->assertWPErrorCodeEquals( 'invalid_meta', $result ); - - // let's force a wp_post_update (wp_insert_post) failure, by forcing the 'wp_insert_post_empty_content' filter - // see wp-includes/post.php:wp_insert_post() - add_filter( 'wp_insert_post_empty_content', '__return_true' ); - - // update should a wp_error object which contains both the 'invalid_meta' error code - // and the 'empty_content' one. - $result = $this->obj->set_bulk( $data, true ); - $this->assertArrayHasKey( 'invalid_meta', $result->errors ); - $this->assertArrayHasKey( 'empty_content', $result->errors ); - - } - -} diff --git a/tests/phpunit/framework/class-llms-post-type-metabox-test-case.php b/tests/phpunit/framework/class-llms-post-type-metabox-test-case.php deleted file mode 100644 index 2855c792b0..0000000000 --- a/tests/phpunit/framework/class-llms-post-type-metabox-test-case.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -// Require main test case. -require_once 'class-llms-unit-test-case.php'; - -/** - * Unit Test Case with tests and utilities specific to testing LifterLMS post type Metabox classes. - * - * @since 3.33.0 -*/ -class LLMS_PostTypeMetaboxTestCase extends LLMS_UnitTestCase { - - /** - * Require all necessary files. - * - * @since 3.33.0 - * @since 3.36.1 Conditionally require LLMS_Admin_Meta_Boxes. - * @since 3.37.12 Call parent method. - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - // Manually include required files. - include_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.fields.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/llms.interface.meta.box.field.php'; - include_once LLMS_PLUGIN_DIR . 'includes/abstracts/abstract.llms.admin.metabox.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.post-types.php'; - if ( ! class_exists( 'LLMS_Admin_Meta_Boxes' ) ) { - ( new LLMS_Admin_Post_Types() )->include_post_type_metabox_class(); - } - - } - - /** - * Metabox utility function to add the metabox nonce field to an array of data. - * - * @since 3.36.1 - * - * @param array $data Data array. - * @param bool $real If true, uses a real nonce. Otherwise uses a fake nonce (useful for testing negative cases). - * @return array - */ - protected function add_nonce_to_array( $data = array(), $real = true ) { - - $nonce_string = $real ? wp_create_nonce( 'lifterlms_save_data' ) : wp_create_nonce( 'fake' ); - - return wp_parse_args( $data, array( - 'lifterlms_meta_nonce' => $nonce_string, - ) ); - - } - -} diff --git a/tests/phpunit/framework/class-llms-settings-page-test-case.php b/tests/phpunit/framework/class-llms-settings-page-test-case.php deleted file mode 100644 index 03da2c1722..0000000000 --- a/tests/phpunit/framework/class-llms-settings-page-test-case.php +++ /dev/null @@ -1,171 +0,0 @@ -<?php -/** - * LifterLMS Unit Test Case Base class - * - * @since 3.37.3 - */ -class LLMS_Settings_Page_Test_Case extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.37.3 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->page = new $this->classname(); - - } - - /** - * Stub to be overridden in extending classes. - * - * This function should return an array of arrays. - * - * The array key is the option id and the value is an array of possible values to store. - * - * @since 3.37.3 - * - * @return array[] - */ - protected function get_mock_settings() { - return array(); - } - - /** - * Retrieve an indexed array of ids for the page's registered settings. - * - * @since 3.37.3 - * - * @param bool $save_only If `true`, only return fields that can be saved to the database. - * @return string[] - */ - protected function get_settings_ids( $save_only = true ) { - - $saveable = array( 'checkbox', 'textarea', 'wpeditor', 'password', 'text', 'email', 'number', 'select', 'single_select_page', 'single_select_membership', 'radio', 'hidden', 'image', 'multiselect' ); - - $ids = array(); - foreach ( $this->page->get_settings() as $setting ) { - if ( empty( $setting['id'] ) || ( $save_only && ! in_array( $setting['type'], $saveable, true ) ) ) { - continue; - } - $ids[] = $setting['id']; - } - return $ids; - - } - - /** - * Test the settings page ID matches the expected ID. - * - * @since 3.37.3 - * - * @return void - */ - public function test_id() { - $this->assertEquals( $this->class_id, $this->page->id ); - } - - /** - * Test the settings page label matches the expected label. - * - * @since 3.37.3 - * - * @return void - */ - public function test_label() { - $this->assertEquals( $this->class_label, $this->page->label ); - } - - /** - * Ensure all editable settings exist in the settings array. - * - * @since 3.37.3 - * - * @return [type] - */ - public function test_get_settings() { - - $settings = $this->get_mock_settings(); - - if ( ! $settings ) { - $this->markTestSkipped( 'No mock setting registered to test.' ); - } - - $mock = array_keys( $settings ); - $actual = $this->get_settings_ids(); - $this->assertEquals( $mock, $actual ); - - } - - /** - * Ensure no duplicate values exist in the settings array. - * - * @since 3.37.3 - * - * @return void - */ - public function test_get_settings_dupcheck() { - - $actual = $this->get_settings_ids( false ); - $no_dupes = array_unique( $actual ); - $this->assertEquals( $no_dupes, $actual ); - - } - - /** - * Test the save() method. - * - * @since 3.37.3 - * - * @return void - */ - public function test_save() { - - $settings = $this->get_mock_settings(); - - if ( ! $settings ) { - $this->markTestSkipped( 'No mock setting registered to test.' ); - } - - $post = array(); - foreach ( $settings as $key => $vals ) { - $post[ $key ] = $vals[0]; - - foreach ( $vals as $val ) { - - $this->mockPostRequest( array( - $key => $val, - ) ); - $this->page->save(); - $this->assertEquals( $val, get_option( $key ), $key ); - - } - - } - - // Bulk save all of them at once. - $this->mockPostRequest( $post ); - $this->page->save(); - foreach ( $post as $key => $val ) { - $this->assertEquals( $val, get_option( $key ), $key ); - } - - } - - /** - * Test the set_label() method. - * - * @since 3.37.3 - * - * @return void - */ - public function test_set_label() { - $this->assertEquals( $this->class_label, LLMS_Unit_Test_Util::call_method( $this->page, 'set_label' ) ); - } - -} diff --git a/tests/phpunit/framework/class-llms-shortcode-test-case.php b/tests/phpunit/framework/class-llms-shortcode-test-case.php deleted file mode 100644 index 6c4c904f66..0000000000 --- a/tests/phpunit/framework/class-llms-shortcode-test-case.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Unit Test Case with tests and utilities specific to testing LifterLMS Shortcodes - * - * @since 3.24.1 - * @since 5.0.0 Add helper method `get_class()`. - * @version 5.0.0 - */ - -require_once 'class-llms-unit-test-case.php'; - -class LLMS_ShortcodeTestCase extends LLMS_UnitTestCase { - - /** - * Class name of the Shortcode Class - * - * @var string - */ - public $class_name = ''; - - /** - * Retrieve an instance of the shortcode generator class. - * - * @since 5.0.0 - * - * @return obj - */ - protected function get_class() { - - return call_user_func( array( $this->class_name, 'instance' ) ); - - } - - /** - * Assertion to expect the output of a given shortcode string. - * - * @since 5.0.0 - * - * @param string $expect Expected shortcode output. - * @param string $shortcode Shortcode string (to be wrapped in `do_shortcode()`). - * @return void - */ - protected function assertShortcodeOutputEquals( $expect, $shortcode ) { - - ob_start(); - echo do_shortcode( $shortcode ); - $actual = ob_get_clean(); - - return $this->assertEquals( $expect, $actual ); - - } - - /** - * Test shortcode registration - * - * @since 3.24.1 - * @since 3.24.3 Unknown. - * - * @return void - */ - public function test_registration() { - - $obj = $this->get_class(); - $this->assertTrue( shortcode_exists( $obj->tag ) ); - $this->assertTrue( is_a( $obj, 'LLMS_Shortcode' ) ); - $this->assertTrue( ! empty( $obj->tag ) ); - $this->assertTrue( is_string( $obj->output() ) ); - $this->assertTrue( is_array( $obj->get_attributes() ) ); - $this->assertTrue( is_string( $obj->get_content() ) ); - - } - -} diff --git a/tests/phpunit/framework/class-llms-unit-test-case.php b/tests/phpunit/framework/class-llms-unit-test-case.php deleted file mode 100644 index 72382cfae4..0000000000 --- a/tests/phpunit/framework/class-llms-unit-test-case.php +++ /dev/null @@ -1,574 +0,0 @@ -<?php -/** - * LifterLMS Unit Test Case Base class - * - * @since 3.3.1 - * @since 3.33.0 Marked `setup_get()` and `setup_post()` as deprecated and removed private `setup_request()`. Use methods from lifterlms/lifterlms_tests. - * @since 3.37.4 Add certificate template mock generation and earning methods. - * @since 3.37.8 Changed return of `take_quiz` method from `void` to an `LLMS_Quiz_Attempt` object - * @since 3.37.17 Added voucher creation method. - * @since 3.38.0 Added `setManualGatewayStatus()` method. - * @since 4.0.0 Added create_mock_session-data() class. - * @since 4.7.0 Disabled image sideloading during mock course generation. - * @since 5.0.0 Automatically clear notices on teardown. - * Add a method to generate mock vouchers. - */ -class LLMS_UnitTestCase extends LLMS_Unit_Test_Case { - - /** - * Setup tests - * Automatically called before each test - * - * @since 3.17.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - llms_reset_current_time(); - } - - /** - * Create mock user session data. - * - * @since 4.0.0 - * - * @param integer $count Number of session to create. - * @param boolean $expired Whether or not the sessions are expired. - * @return int[] - */ - protected function create_mock_session_data( $count = 5, $expired = false ) { - - $sessions = array(); - - global $wpdb; - $i = 1; - while ( $i <= $count ) { - $wpdb->insert( $wpdb->prefix . 'lifterlms_sessions', array( - 'session_key' => LLMS_Unit_Test_Util::call_method( LLMS()->session, 'generate_id' ), - 'data' => serialize( array( microtime() ) ), - 'expires' => $expired ? time() - DAY_IN_SECONDS : time() + DAY_IN_SECONDS, - ), array( '%s', '%s', '%d' ) ); - - $sessions[] = $wpdb->insert_id; - - ++$i; - - } - - return $sessions; - - } - - /** - * Setup Get data to mock post and request data - * - * @since 3.19.0 - * @deprecated 3.33.0 Use $this->mockGetRequest() from lifterlms/lifterlms-tests lib. - * - * @param array $vars mock get data - * @return void - */ - protected function setup_get( $vars = array() ) { - $this->mockGetRequest( $vars ); - } - - /** - * Setup Post data to mock post and request data - * - * @since 3.19.0 - * @deprecated 3.33.0 Use $this->mockPostRequest() from lifterlms/lifterlms-tests lib. - * - * @param array $vars mock post data. - * @return void - */ - protected function setup_post( $vars = array() ) { - $this->mockPostRequest( $vars ); - } - - /** - * Automatically complete a percentage of courses for a student - * @param integer $student_id WP User ID of a student - * @param array $course_ids array of WP Post IDs for the courses - * @param integer $perc percentage of each course complete - * percentage is based off the total number of lessons in the course - * fractions will be rounded up - * @return void - * @since 3.7.3 - * @version 3.24.0 - */ - protected function complete_courses_for_student( $student_id = 0, $course_ids = array(), $perc = 100 ) { - - if ( ! $student_id ) { - $student = $this->get_mock_student(); - } else { - $student = llms_get_student( $student_id ); - } - - if ( ! is_array( $course_ids ) ) { - $course_ids = array( $course_ids ); - } - - foreach ( $course_ids as $course_id ) { - - $course = llms_get_post( $course_id ); - - // enroll the student if not already enrolled - if ( ! $student->is_enrolled( $course_id ) ) { - $student->enroll( $course_id ); - } - - $lessons = $course->get_lessons( 'ids' ); - $num_lessons = count( $lessons ); - $stop = 100 === $perc ? $num_lessons : round( ( $perc / 100 ) * $num_lessons ); - - foreach ( $lessons as $i => $lid ) { - - // stop once we reach the stopping point - if ( $i + 1 > $stop ) { - break; - } - - $lesson = llms_get_post( $lid ); - if ( $lesson->has_quiz() ) { - - $this->take_quiz( $lesson->get( 'quiz' ), $student->get_id() ); - - } else { - - $student->mark_complete( $lid, 'lesson' ); - - } - - } - - } - - } - - /** - * Create a voucher. - * - * @since 3.37.17 - * - * @param int $codes Number of codes to generate for the voucher. - * @param int $uses Number of uses per code. - * @param int[] $products List of course/membership ids. - * @return LLMS_Voucher - */ - protected function create_voucher( $codes = 5, $uses = 5, $products = array() ) { - - // Create the Voucher Post. - $post_id = $this->factory->post->create( array( 'post_type' => 'llms_voucher' ) ); - $voucher = new LLMS_Voucher( $post_id ); - - // Generate voucher codes. - $i = 0; - while( $i < $codes ) { - $voucher->save_voucher_code( array( - 'code' => substr( bin2hex( random_bytes( 12 ) ), 0, 12 ), - 'redemption_count' => $uses, - ) ); - ++$i; - } - - // Add a mock course if no products are specified. - if ( ! $products ) { - $products[] = $this->factory->post->create( array( 'post_type' => 'course' ) ); - } - - // Save the products. - foreach ( $products as $product ) { - $voucher->save_product( $product ); - } - - return $voucher; - - } - - /** - * Take a quiz for a student and get a desired grade - * - * @since 3.24.0 - * @since 3.37.8 Change return from `void` to an `LLMS_Quiz_Attempt` object - * - * @param int $quiz_id WP Post ID of the Quiz. - * @param int $student_id WP Used ID of the student. - * @param int $grade Desired grade. Do the math in the test, this can't make the grade happen if it's not possible - * for example a quiz with 5 questions CANNOT get a 75%! - * - * @return LLMS_Quiz_Attempt - */ - public function take_quiz( $quiz_id, $student_id, $grade = 100 ) { - - $quiz = llms_get_post( $quiz_id ); - $student = llms_get_student( $student_id ); - - $attempt = LLMS_Quiz_Attempt::init( $quiz_id, $quiz->get( 'lesson_id' ), $student_id )->start(); - - $questions_count = $attempt->get_count( 'gradeable_questions' ); - $points_per_question = ( 100 / $questions_count ); - $to_be_correct = $grade / $points_per_question; - - $i = 1; - while ( $attempt->get_next_question() ) { - - $question_id = $attempt->get_next_question(); - - $question = llms_get_post( $question_id ); - $correct = $question->get_correct_choice(); - // select the correct answer - if ( $i <= $to_be_correct ) { - - $selected = $correct; - - // select a random incorrect answer - } else { - - // filter all correct choices out of the array of choices - $options = array_filter( $question->get_choices(), function( $choice ) { - return ( ! $choice->is_correct() ); - } ); - - // rekey - $options = array_values( $options ); - - // select a random incorrect answer - $selected = array( $options[ rand( 0, count( $options ) - 1 ) ]->get( 'id' ) ); - - } - - $attempt->answer_question( $question_id, $selected ); - - $i++; - - } - - $attempt->end(); - - return $attempt; - - } - - /** - * Generates a set of mock courses - * - * @since 3.7.3 - * @since 4.7.0 Disabled image sideloading during mock course generation. - * - * @param integer $num_courses number of courses to generate - * @param integer $num_sections number of sections to generate for each course - * @param integer $num_lessons number of lessons to generate for each section - * @param integer $num_quizzes number of quizzes to generate for each section - * quizzes will be attached to the last lessons ie each section - * if you generate 3 lessons / section and 1 quiz / section the quiz - * will always be the 3rd lesson - * @return array indexed array of course ids - */ - protected function generate_mock_courses( $num_courses = 1, $num_sections = 5, $num_lessons = 5, $num_quizzes = 1, $num_questions = 5 ) { - - $courses = array(); - $i = 1; - while ( $i <= $num_courses ) { - $courses[] = $this->get_mock_course_array( $i, $num_sections, $num_lessons, $num_quizzes, $num_questions ); - $i++; - } - - add_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - - $gen = new LLMS_Generator( array( 'courses' => $courses ) ); - $gen->set_generator( 'LifterLMS/BulkCourseGenerator' ); - $gen->set_default_post_status( 'publish' ); - $gen->generate(); - - remove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - if ( ! $gen->is_error() ) { - return $gen->get_generated_courses(); - } - - } - - /** - * Generates an array of course data which can be passed to a Generator - * @param int $iterator number for use as course number - * @param int $num_sections number of sections to generate for the course - * @param int $num_lessons number of lessons for each section in the course - * @param int $num_quizzes number of quizzes for each section in the course - * @return array - * @since 3.7.3 - * @version 3.16.12 - */ - protected function get_mock_course_array( $iterator = 1, $num_sections = 3, $num_lessons = 5, $num_quizzes = 1, $num_questions = 5 ) { - - $mock = array( - 'title' => sprintf( 'mock course %d', $iterator ), - ); - - $sections = array(); - $sections_i = 1; - while ( $sections_i <= $num_sections ) { - - $section = array( - 'title' => sprintf( 'mock section %d', $sections_i ), - 'lessons' => array(), - ); - - $lessons_i = 1; - - $quizzes_start_i = $num_lessons - $num_quizzes + 1; - - while ( $lessons_i <= $num_lessons ) { - - $lesson = array( - 'title' => sprintf( 'mock lesson %d', $lessons_i ), - ); - - if ( $lessons_i >= $quizzes_start_i ) { - - $lesson['quiz_enabled'] = 'yes'; - - $lesson['quiz'] = array( - 'title' => sprintf( 'mock quiz %d', $lessons_i ), - ); - - $questions = array(); - $questions_i = 1; - while ( $questions_i <= $num_questions ) { - - $options_i = 1; - $total_options = rand( 2, 5 ); - $correct_option = rand( $options_i, $total_options ); - $choices = array(); - while( $options_i <= $total_options ) { - $choices[] = array( - 'choice' => sprintf( 'choice %d', $options_i ), - 'choice_type' => 'text', - 'correct' => ( $options_i === $correct_option ), - ); - $options_i++; - } - $questions[] = array( - 'title' => sprintf( 'question %d', $questions_i ), - 'question_type' => 'choice', - 'choices' => $choices, - 'points' => 1, - ); - - $questions_i++; - - } - - $lesson['quiz']['questions'] = $questions; - - } - - array_push( $section['lessons'], $lesson ); - $lessons_i++; - } - - array_push( $sections, $section ); - - $sections_i++; - - } - - $mock['sections'] = $sections; - - return $mock; - - } - - protected function get_mock_order( $plan = null, $coupon = false ) { - - $gateway = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $gateway->get_option_name( 'enabled' ), 'yes' ); - - if ( ! $plan ) { - if ( ! $this->saved_mock_plan ) { - $plan = $this->get_mock_plan(); - $this->saved_mock_plan = $plan; - } else { - $plan = $this->saved_mock_plan; - } - } - - if ( $coupon ) { - $coupon = new LLMS_Coupon( 'new', 'couponcode' ); - $coupon_data = array( - 'coupon_amount' => 10, - 'discount_type' => 'percent', - 'plan_type' => 'any', - ); - foreach ( $coupon_data as $key => $val ) { - $coupon->set( $key, $val ); - } - } - - $order = new LLMS_Order( 'new' ); - return $order->init( $this->get_mock_student(), $plan, $gateway, $coupon ); - - } - - /** - * Retrieve a mock access plan - * - * Automatically generates a course associated with the plan. - * - * @since 3.38.0 - * - * @param float $price Plan price. - * @param integer $frequency Recurring frequency. - * @param string $expiration Plan expiration. - * @param boolean $on_sale Whether or not the plan is on sale. - * @param boolean $trial whether or not the plan has a trial. - * @return LLMS_Access_Plan - */ - protected function get_mock_plan( $price = 25.99, $frequency = 1, $expiration = 'lifetime', $on_sale = false, $trial = false ) { - - $course = $this->generate_mock_courses( 1, 0 ); - $course_id = $course[0]; - - $plan = new LLMS_Access_Plan( 'new', 'Test Access Plan' ); - $plan_data = array( - 'access_expiration' => $expiration, - 'access_expires' => ( 'limited-date' === $expiration ) ? date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) : '', - 'access_length' => '1', - 'access_period' => 'year', - 'frequency' => $frequency, - 'is_free' => $price > 0 ? 'no' : 'yes', - 'length' => 0, - 'on_sale' => $on_sale ? 'yes' : 'no', - 'period' => 'day', - 'price' => $price, - 'product_id' => $course_id, - 'sale_price' => round( $price - ( $price * .1 ), 2 ), - 'sku' => 'accessplansku', - 'trial_length' => 1, - 'trial_offer' => $trial ? 'yes' : 'no', - 'trial_period' => 'week', - 'trial_price' => 1.00, - ); - - foreach ( $plan_data as $key => $val ) { - $plan->set( $key, $val ); - } - - return $plan; - - } - - /** - * Generate a mock voucher. - * - * @since 5.0.0 - * - * @param int $codes Number of codes to create for the voucher. - * @param int $uses Number of uses for each code. - * @param array $products Array of WP_Post IDs to associate with voucher. - * @return LLMS_Voucher - */ - protected function get_mock_voucher( $codes = 5, $uses = 1, $products = array() ) { - - $voucher_id = $this->factory->post->create( array( 'post_type' => 'llms_voucher' ) ); - $voucher = new LLMS_Voucher( $voucher_id ); - - if ( ! $products ) { - $products = array( $this->factory->course->create( array( 'sections' => 0 ) ) ); - } - - array_map( array( $voucher, 'save_product' ), $products ); - - $i = 1; - while( $i <= $codes ) { - $voucher->save_voucher_code( array( - 'code' => wp_generate_password( 12, false ), - 'redemption_count' => $uses, - ) ); - ++$i; - } - - return $voucher; - - } - - protected function get_mock_student( $login = false ) { - $student_id = $this->factory->user->create( array( 'role' => 'student' ) ); - if ( $login ) { - wp_set_current_user( $student_id ); - } - return llms_get_student( $student_id ); - } - - - /** - * Create a certificate template post. - * - * @since 3.37.4 - * - * @param string $title Certificate title. - * @param string $content Certificate content. - * @param string $image Certificate background image path. - * @return int - */ - protected function create_certificate_template( $title = 'Mock Certificate Title', $content = '', $image = '' ) { - - $template = $this->factory->post->create( array( - 'post_type' => 'llms_certificate', - 'post_content' => $content ? $content : '{site_title}, {current_date}', - ) ); - update_post_meta( $template, '_llms_certificate_title', $title ); - update_post_meta( $template, '_llms_certificate_image', $image ); - - return $template; - - } - - /** - * Earn a certificate for a user. - * - * @since 3.37.3 - * @since 3.37.4 Moved to `LLMS_UnitTestCase`. - * - * @param int $user WP_User ID. - * @param int $template WP_Post ID of the `llms_certificate` template. - * @param int $related WP_Post ID of the related post. - * @return int[] { - * Indexed array containing information about the earned certificate. - * int $0 WP_User ID - * int $1 WP_Post ID of the earned cert (`llms_my_certificate`) - * int $2 WP_Post ID of the related post. - * } - */ - protected function earn_certificate( $user, $template, $related ) { - - global $llms_user_earned_certs; - $llms_user_earned_certs = array(); - - // Watch for generation so we can compare against it later. - add_action( 'llms_user_earned_certificate', function( $user_id, $cert_id, $related_id ) { - global $llms_user_earned_certs; - $llms_user_earned_certs[] = array( $user_id, $cert_id, $related_id ); - }, 10, 3 ); - - LLMS()->certificates()->trigger_engagement( $user, $template, $related ); - - return array_shift( $llms_user_earned_certs ); - - } - - /** - * Toggle the status of the manual payment gateway. - * - * @since 3.38.0 - * - * @param string $enabled Status of the gateway, "yes" for enabled and "no" for disabled. - */ - protected function setManualGatewayStatus( $enabled = 'yes' ) { - - $manual = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $manual->get_option_name( 'enabled' ), $enabled ); - - } - -} diff --git a/tests/phpunit/framework/functions-llms-tests.php b/tests/phpunit/framework/functions-llms-tests.php deleted file mode 100644 index 3f7ec1e9ba..0000000000 --- a/tests/phpunit/framework/functions-llms-tests.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Set the mocked current time - * @param mixed $time date time string parsable by date() - * @return void - * @since 3.4.0 - * @version 3.28.0 - * @deprecated 3.28.0 - */ -function llms_mock_current_time( $time ) { - llms_tests_mock_current_time( $time ); -} - -/** - * Reset current time after mocking it - * @return void - * @since 3.16.0 - * @version 3.28.0 - * @deprecated 3.28.0 - */ -function llms_reset_current_time() { - llms_tests_reset_current_time(); -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-metabox.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-metabox.php deleted file mode 100644 index 0e3c475637..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-metabox.php +++ /dev/null @@ -1,398 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Metabox class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group metaboxes - * @group metabox_abstract - * - * @since 3.37.12 - */ -class LLMS_Test_Admin_Metabox extends LLMS_PostTypeMetaboxTestCase { - - /** - * Retrieve an mocked abstract. - * - * @since 3.37.12 - * - * @return LLMS_Admin_Metabox - */ - private function get_stub() { - - $stub = $this->getMockForAbstractClass( 'LLMS_Admin_Metabox' ); - - $stub->title = 'Mock Metabox'; - $stub->id = 'mocker'; - - return $stub; - - } - - /** - * Mock the get_fields() method for an LLMS_Admin_Metabox stub. - * - * @since 3.37.12 - * - * @param LLMS_Admin_Metabox $stub Metabox stub. - * @return array Array of metabox field data. - */ - private function add_fields_to_stub( $stub ) { - - $fields = array( - array( - 'title' => 'Tab Title', - 'fields' => array( - array( - 'label' => 'Field Title.', - 'desc' => 'Field Description', - 'id' => $stub->prefix . 'mock_field', - 'type' => 'text', - ), - array( - 'label' => 'Field Title.', - 'desc' => 'Field Description', - 'id' => $stub->prefix . 'mock_field_2', - 'type' => 'text', - ), - array( - 'label' => 'Allow quotes Field Title.', - 'desc' => 'Field Description', - 'id' => $stub->prefix . 'mock_field_with_quotes', - 'type' => 'text', - 'sanitize' => 'shortcode', - ), - array( - 'label' => 'Allow quotes Field Title.', - 'desc' => 'Field Description', - 'id' => $stub->prefix . 'mock_field_with_quotes_2', - 'type' => 'text', - 'sanitize' => 'no_encode_quotes', - ), - array( - 'label' => 'Multi Select Title.', - 'desc' => 'Field Description', - 'id' => $stub->prefix . 'mock_field_multi_select', - 'type' => 'select', - 'multi' => true, - 'value' => array( - 'key_1' => 'Value 1', - 'key_2' => 'Value 2', - 'key_3' => 'Value 3', - ), - ), - ), - ), - ); - - $stub->method( 'get_fields' )->will( $this->returnValue( $fields ) ); - - return $fields; - - } - - /** - * Test add_error(), get_errors(), has_errors(), and save_errors(). - * - * @since 3.37.12 - * - * @return void. - */ - public function test_errors_get_set_save() { - - $stub = $this->get_stub(); - - // No messages. - $this->assertEquals( array(), $stub->get_errors() ); - $this->assertEquals( false, $stub->has_errors() ); - - // Has a message. - $stub->add_error( 'Error message.' ); - $this->assertEquals( true, $stub->has_errors() ); - $stub->save_errors(); - $this->assertEquals( array( 'Error message.' ), $stub->get_errors() ); - - // Has 2 messages. - $stub->add_error( 'Second message.' ); - $this->assertEquals( true, $stub->has_errors() ); - $stub->save_errors(); - $this->assertEquals( array( 'Error message.', 'Second message.' ), $stub->get_errors() ); - - } - - /** - * Test get_screens() method. - * - * @since 3.37.12 - * - * @return void - */ - public function test_get_screens() { - - $stub = $this->get_stub(); - - // As string. - $stub->screens = 'post'; - $this->assertEquals( array( 'post' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) ); - - // Array. - $stub->screens = array( 'post' ); - $this->assertEquals( array( 'post' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) ); - - // Array with multiple post types. - $stub->screens[] = 'page'; - $this->assertEquals( array( 'post', 'page' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) ); - - } - - /** - * Test save(): no nonce supplied. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_no_nonce() { - - $stub = $this->get_stub(); - $post = $this->factory->post->create(); - - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - } - - /** - * Test save(): invalid nonce supplied. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_invalid_nonce() { - - $stub = $this->get_stub(); - $post = $this->factory->post->create(); - - $this->mockPostRequest( $this->add_nonce_to_array( array(), false ) ); - - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - } - - /** - * Test save(): missing required capabilites. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_no_cap() { - - $stub = $this->get_stub(); - $post = $this->factory->post->create(); - - $this->mockPostRequest( $this->add_nonce_to_array() ); - - // Logged out. - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - // Invalid cap. - wp_set_current_user( $this->factory->student->create() ); - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - } - - /** - * Test save(): during a quick edit (inline save). - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_inline_save() { - - $stub = $this->get_stub(); - $post = $this->factory->post->create(); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( $this->add_nonce_to_array( array( - 'action' => 'inline-save', - ) ) ); - - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - } - - /** - * Test save(): for a metabox with no fields. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_no_fields() { - - $stub = $this->get_stub(); - $post = $this->factory->post->create(); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( $this->add_nonce_to_array( array() ) ); - - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - } - - /** - * Test save(): when it all works. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_success() { - - $stub = $this->get_stub(); - $this->add_fields_to_stub( $stub ); - $post = $this->factory->post->create(); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Save. - $this->mockPostRequest( $this->add_nonce_to_array( array( - $stub->prefix . 'mock_field' => 'mock_val_1', - $stub->prefix . 'mock_field_2' => 'mock_val_2', - ) ) ); - - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - $this->assertEquals( 'mock_val_1', get_post_meta( $post, $stub->prefix . 'mock_field', true ) ); - $this->assertEquals( 'mock_val_2', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) ); - - // Unset values that aren't posted. - $this->mockPostRequest( $this->add_nonce_to_array( array( - $stub->prefix . 'mock_field' => 'mock_val_1', - ) ) ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - $this->assertEquals( 'mock_val_1', get_post_meta( $post, $stub->prefix . 'mock_field', true ) ); - $this->assertEquals( '', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) ); - - // Unset a value, update another. - $this->mockPostRequest( $this->add_nonce_to_array( array( - $stub->prefix . 'mock_field' => '', - $stub->prefix . 'mock_field_2' => 'new_Val', - ) ) ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) ); - - $this->assertEquals( '', get_post_meta( $post, $stub->prefix . 'mock_field', true ) ); - $this->assertEquals( 'new_Val', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) ); - - } - - /** - * Test save_field() for a standard field (text) - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_field_standard() { - - $stub = $this->get_stub(); - $field = $this->add_fields_to_stub( $stub )[0]['fields'][0]; - $post = $this->factory->post->create(); - - $this->mockPostRequest( array( - $field['id'] => 'Saved "Field" Value.', - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( 'Saved "Field" Value.', get_post_meta( $post, $field['id'], true ) ); - - // Unset the value. - $this->mockPostRequest( array() ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( '', get_post_meta( $post, $field['id'], true ) ); - - - } - - /** - * Test save_field() for "shortcode" sanitization. - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_field_allow_quotes() { - - $stub = $this->get_stub(); - $fields = $this->add_fields_to_stub( $stub ); - $post = $this->factory->post->create(); - - foreach ( array( 2, 3 ) as $index ) { - - $field = $fields[0]['fields'][ $index ]; - - $this->mockPostRequest( array( - $field['id'] => 'Saved "Field" Value.', - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( 'Saved "Field" Value.', get_post_meta( $post, $field['id'], true ) ); - - } - - } - - /** - * Test save_field() for a multi-select - * - * @since 3.37.12 - * - * @return void - */ - public function test_save_field_multi_select() { - - $stub = $this->get_stub(); - $field = $this->add_fields_to_stub( $stub )[0]['fields'][4]; - $post = $this->factory->post->create(); - - // Array not submitted. - $this->mockPostRequest( array( - $field['id'] => 'key_1', - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( '', get_post_meta( $post, $field['id'], true ) ); - - // Single value. - $this->mockPostRequest( array( - $field['id'] => array( 'key_1' ), - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( array( 'key_1' ), get_post_meta( $post, $field['id'], true ) ); - - // Multi values. - $this->mockPostRequest( array( - $field['id'] => array( 'key_1', 'key_2' ), - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( array( 'key_1', 'key_2' ), get_post_meta( $post, $field['id'], true ) ); - - - // Unset. - $this->mockPostRequest( array( - $field['id'] => array(), - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) ); - $this->assertEquals( array(), get_post_meta( $post, $field['id'], true ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-tool.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-tool.php deleted file mode 100644 index cff7c99570..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-tool.php +++ /dev/null @@ -1,185 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Admin_Tool class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group admin - * @group admin_tools - * - * @since 3.37.19 - */ -class LLMS_Test_Abstract_Admin_Tool extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * Include abstract class. - * - * @since 3.37.19 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - require_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-admin-tool.php'; - - } - - /** - * Retrieve a mock for the abstract class. - * - * @since 3.37.19 - * - * @return LLMS_Abstract_Admin_Tool - */ - private function get_abstract_mock() { - - $mock = $this->getMockForAbstractClass( 'LLMS_Abstract_Admin_Tool' ); - LLMS_Unit_Test_Util::set_private_property( $mock, 'id', 'mock' ); - - remove_filter( 'llms_status_tools', array( $mock, 'register' ) ); - remove_action( 'llms_status_tool', array( $mock, 'maybe_handle' ) ); - - return $mock; - - } - - /** - * Retrieve a "concrete" mock with the abstract methods defined. - * - * @since 3.37.19 - * - * @param boolean $load The mock return of `should_load()`. - * @return LLMS_Abstract_Admin_Tool - */ - private function get_concrete_mock( $load = true, $handle = true ) { - - // Gross. - global $llms_mock_temp_load; - $llms_mock_temp_load = $load; - - $mock = new class extends LLMS_Abstract_Admin_Tool { - protected $id = 'mock'; - public function should_load() { - // Disgusting. - global $llms_mock_temp_load; - return $llms_mock_temp_load; - } - protected function handle() { return true; } - protected function get_description() { return 'Description'; } - protected function get_label() { return 'Label'; } - protected function get_text() { return 'Text'; } - }; - - // Ehck. - unset( $llms_mock_temp_load ); - - return $mock; - - } - - /** - * Test the constructor when the tool should load. - * - * @since 3.37.19 - * - * @return void - */ - public function test_constructor_should_load() { - - $tool = $this->get_abstract_mock(); - $tool->__construct(); - - $this->assertEquals( 10, has_filter( 'llms_status_tools', array( $tool, 'register' ) ) ); - $this->assertEquals( 10, has_action( 'llms_status_tool', array( $tool, 'maybe_handle' ) ) ); - - } - - /** - * Test maybe_handle() should_load() condition - * - * @since 3.37.19 - * - * @return void - */ - public function test_maybe_handle_check_should_load() { - - $tool = $this->get_concrete_mock( true ); - $this->assertTrue( $tool->maybe_handle( 'mock' ) ); - - $tool = $this->get_concrete_mock( false ); - $this->assertFalse( $tool->maybe_handle( 'mock' ) ); - - } - - /** - * Test maybe_handle() ensure the id matches. - * - * @since 3.37.19 - * - * @return void - */ - public function test_maybe_handle_check_ids() { - - $tool = $this->get_concrete_mock(); - - $this->assertFalse( $tool->maybe_handle( 'fake' ) ); - $this->assertTrue( $tool->maybe_handle( 'mock' ) ); - - } - - /** - * Test register() when the tool should load. - * - * @since 3.37.19 - * - * @return void - */ - public function test_register() { - - $tool = $this->get_concrete_mock(); - $this->assertEquals( array( - 'mock' => array( - 'description' => 'Description', - 'label' => 'Label', - 'text' => 'Text', - ), - ), $tool->register( array() ) ); - - } - - /** - * Test register() when the tool should not load. - * - * @since 3.37.19 - * - * @return void - */ - public function test_register_no_load() { - - $tool = $this->get_concrete_mock( false ); - - $this->assertEquals( array(), $tool->register( array() ) ); - - } - - /** - * Test should_load() stub. - * - * @since 3.37.19 - * - * @return void - */ - public function test_should_load() { - - $tool = $this->get_abstract_mock(); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $tool, 'should_load' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-database-query.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-database-query.php deleted file mode 100644 index 2c858663bd..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-database-query.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php -/** - * Tests for the LLMS_Database_Query class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group query - * @group dbquery - * - * @since 4.5.1 - */ -class LLMS_Test_Database_Query extends LLMS_UnitTestCase { - - private $_arguments_original; - - /** - * Cleanup on tear_down - * - * @since 4.5.1 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - global $wpdb; - $wpdb->query( "TRUNCATE {$wpdb->posts}" ); - } - - /** - * Test that by default the query args has no_found_rows set to false - * - * @since 4.5.1 - * - * @return void - */ - public function test_default_args_no_found_rows_false() { - $query = $this->query(); - $args = LLMS_Unit_Test_Util::call_method( $query, 'get_default_args' ); - $this->assertEquals( false, $args['no_found_rows'] ); - } - - /** - * Test that by default the query found_results and max_pages are not empty (when there are results) - * - * This is because no_found_rows by default is false. - * - * @since 4.5.1 - * - * @return void - */ - public function test_found_rows_max_pages_not_empty() { - // Create some posts to have some element in our test table. - $this->factory->post->create_many(8); - - $query = $this->query(); - $this->assertEquals( 8, $query->found_results ); - $this->assertEquals( 1, $query->max_pages ); - } - - /** - * Test when found rows and max pages are not set - * - * @since 4.5.1 - * - * @return void - */ - public function test_found_rows_max_pages_empty() { - // No results, no found_results no max_pages are set. - $query = $this->query(); - $this->assertFalse( $query->has_results() ); - $this->assertEquals( 0, $query->found_results ); - $this->assertEquals( 0, $query->max_pages ); - - // Create some posts to have some element in our test table. - $this->factory->post->create_many(8); - - // Query but avoiding calculating found rows. - $query = $this->query( - array( - 'no_found_rows' => true, - ) - ); - - // We have results but no found_results no max_pages are set. - $this->assertTrue( $query->has_results() ); - $this->assertEquals( 0, $query->found_results ); - $this->assertEquals( 0, $query->max_pages ); - } - - /** - * Test sql_select_columns() method - * - * @since 4.5.1 - * - * @return void - */ - public function test_sql_select_columns() { - - $query = $this->query(); - $this->assertEquals( 'SQL_CALC_FOUND_ROWS *', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' ) ); - $this->assertEquals( 'SQL_CALC_FOUND_ROWS column', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns', array( 'column' ) ) ); - - // Query but avoiding calculating found rows. - $query = $this->query( - array( - 'no_found_rows' => true, - ) - ); - $this->assertEquals( '*', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' ) ); - $this->assertEquals( 'column', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns', array( 'column' ) ) ); - } - - /** - * Build query - * - * @since 4.5.1 - * - * @param array $args Optional. Query arguments. Default empty array. - * When not provided the default arguments will be used. - * @return void - */ - private function query( $args = array() ) { - - add_filter( 'llms_database_query_prepare_query', array( $this, '_prepare_query' ), 10, 2 ); - if ( ! empty( $args ) ) { - $this->_arguments_original = $args; - add_filter( 'llms_database_query_parse_args', array( $this, '_parse_args' ), 10, 2 ); - } - - $query = $this->getMockForAbstractClass( 'LLMS_Database_Query'); - - add_filter( 'llms_database_query_prepare_query', array( $this, '_prepare_query' ), 10, 2 ); - if ( ! empty( $args ) ) { - remove_filter( 'llms_database_query_parse_args', array( $this, '_parse_args' ), 10, 2 ); - unset($this->_arguments_original); - } - - return $query; - } - - /** - * Prepare query to build a testable SQL - * - * @since 4.5.1 - * - * @return string - */ - public function _prepare_query( $sql, $query ) { - global $wpdb; - $select = LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' ); - $orderby = LLMS_Unit_Test_Util::call_method( $query, 'sql_orderby' ); - $limit = LLMS_Unit_Test_Util::call_method( $query, 'sql_limit' ); - - return " - SELECT {$select} - FROM {$wpdb->posts} - {$orderby} - {$limit}; - "; - } - - /** - * Parse args - * - * @since 4.5.1 - * - * @return string - */ - public function _parse_args( $args, $query ) { - return wp_parse_args( $this->_arguments_original, $args ); - } -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-generator-posts.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-generator-posts.php deleted file mode 100644 index 69a84747ae..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-generator-posts.php +++ /dev/null @@ -1,705 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Generator_Posts class - * - * @group abstracts - * @group generator - * @group generator_posts - * - * @since 4.7.0 - */ -class LLMS_Test_Abstract_Generator_Posts extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->stub = $this->get_stub(); - - } - - /** - * Retrieve the abstract class mock stub - * - * @since 4.7.0 - * - * @return LLMS_Abstract_Generator_Posts - */ - private function get_stub( $raw = array() ) { - return $this->getMockForAbstractClass( 'LLMS_Abstract_Generator_Posts', array( $raw ) ); - } - - /** - * Test add_custom_values() - * - * @since 4.7.0 - * - * @return void - */ - public function test_add_custom_values() { - - $post_id = $this->factory->post->create(); - - $raw = array( - 'custom' => array( - '_mock_multi' => array( 1, 2, 3, ), - '_mock_single' => array( 'value', ), - '_mock_empty' => array( '', ), - '_mock_serialized' => array( serialize( array( 'data' => true ) ) ), - '_mock_json' => array( '{"data":"string"}' ), - ), - ); - - $this->stub->add_custom_values( $post_id, $raw ); - - $this->assertEquals( array( 1, 2, 3 ), get_post_meta( $post_id, '_mock_multi' ) ); - $this->assertEquals( 'value', get_post_meta( $post_id, '_mock_single', true ) ); - $this->assertEquals( '', get_post_meta( $post_id, '_mock_empty', true ) ); - $this->assertEquals( array( 'data' => true ), get_post_meta( $post_id, '_mock_serialized', true ) ); - $this->assertEquals( '{"data":"string"}', get_post_meta( $post_id, '_mock_json', true ) ); - } - - /** - * Test create_post() success - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_post() { - - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'course', array( 'title' => 'test' ) ) ); - $this->assertInstanceOf( 'LLMS_Course', $res ); - $this->assertEquals( 'test', $res->get( 'title' ) ); - - } - - /** - * Test create_post() for invalid post type classes - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_post_invalid_type() { - - $this->setExpectedException( Exception::class, 'The class "LLMS_Fake_Type" does not exist.', 1100 ); - LLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'fake_type' ) ); - - } - - /** - * Test create_post() when an error is encountered during creation - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_post_error() { - - // Force post creation to fail. - $handler = function( $args ) { - return array(); - }; - add_filter( 'llms_new_course', $handler ); - - $this->setExpectedException( Exception::class, 'Error creating the course post object.', 1000 ); - LLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'course', array( 'title' => '' ) ) ); - - remove_filter( 'llms_new_course', $handler ); - - } - - /** - * Test create_reusable_block() when the block already exists - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_reusable_block_already_exists() { - - $title = 'Dupcheck reuse block'; - $content = 'Block content'; - - $dup = $this->factory->post->create( array( - 'post_type' => 'wp_block', - 'post_title' => $title, - 'post_content' => $content, - ) ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block', array( $dup, compact( 'title', 'content' ) ) ) ); - - } - - /** - * Test create_reusable_block() when there's an error creating the block - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_reusable_block_error() { - - // Force an error response. - add_filter( 'wp_insert_post_empty_content', '__return_true' ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block', array( $this->factory->post->create(), array( - 'title' => '', - 'content' => '', - ) ) ) ); - - remove_filter( 'wp_insert_post_empty_content', '__return_true' ); - - } - - /** - * Test create_reusable_block() for success - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_reusable_block_success() { - - $orig_id = $this->factory->post->create(); - - $title = 'Reusable block title'; - $content = 'Reusable block content'; - - $id = LLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block', array( $orig_id, compact( 'title', 'content' ) ) ); - $post = get_post( $id ); - - $this->assertTrue( is_numeric( $id ) ); - $this->assertEquals( 'wp_block', $post->post_type ); - $this->assertEquals( $title, $post->post_title ); - $this->assertEquals( $content, $post->post_content ); - - $blocks = LLMS_Unit_Test_Util::get_private_property_value( $this->stub, 'reusable_blocks' ); - $this->assertEquals( $id, $blocks[ $orig_id ] ); - - } - - /** - * Test format_date() - * - * @since 4.7.0 - * - * @return void - */ - public function test_format_date() { - - // No date supplied, use current time. - $expect = '2020-03-25 09:54:12'; - llms_tests_mock_current_time( $expect ); - - $this->assertEquals( $expect, $this->stub->format_date() ); - - llms_tests_reset_current_time(); - - // Format is okay. - $this->assertEquals( '2015-03-02 23:12:32', $this->stub->format_date( '2015-03-02 23:12:32' ) ); - - // Missing time. - $this->assertEquals( '2019-01-01 00:00:00', $this->stub->format_date( '2019-01-01' ) ); - - // Valid format. - $this->assertEquals( '2019-01-01 00:00:00', $this->stub->format_date( 'January 1, 2019' ) ); - - } - - public function test_get_author_id_no_id_or_email() { - - $uid = $this->factory->user->create(); - wp_set_current_user( $uid ); - - $this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array() ) ) ); - - } - - public function test_get_author_id() { - - $email = 'mockauthor@test.tld'; - $uid = $this->factory->user->create( array( 'user_email' => $email ) ); - - // Only email. - $this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'email' => $email, - ) ) ) ); - - // Only ID. - $this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'id' => $uid, - ) ) ) ); - - // ID & EMail and the email matches the existing user. - $this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'id' => $uid, - 'email' => $email, - ) ) ) ); - - // ID & email and the email does not match the existing user. - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'id' => $uid, - 'email' => 'adifferentemail@test.tld', - ) ) ); - $this->assertEquals( 'adifferentemail@test.tld', get_user_by( 'ID', $res )->user_email ); - - // User doesn't exist, create a new one. - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'id' => $res + 1, - 'email' => 'anotheremail@test.tld', - ) ) ); - $this->assertEquals( 'anotheremail@test.tld', get_user_by( 'ID', $res )->user_email ); - - // Email only, create a new user. - $raw = array( - 'email' => 'el_duderino@earthlink.net', - 'first_name' => 'Jeffrey', - 'last_name' => 'Lebowski', - 'description' => "Nobody calls me Lebowski. You got the wrong guy. I'm the Dude, man.", - ); - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( $raw ) ); - $user = get_user_by( 'ID', $res ); - $this->assertEquals( $raw['email'], $user->user_email ); - $this->assertEquals( $raw['first_name'] . ' ' . $raw['last_name'], $user->display_name ); - $this->assertEquals( $raw['first_name'], $user->first_name ); - $this->assertEquals( $raw['last_name'], $user->last_name ); - $this->assertEquals( $raw['description'], $user->description ); - $this->assertTrue( $user->has_cap( 'administrator' ) ); // Default role. - - // Pass in a role. - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( - 'email' => 'instructoruser@test.tld', - 'role' => 'instructor', - ) ) ); - $this->assertTrue( get_user_by( 'ID', $res )->has_cap( 'instructor' ) ); - - } - - /** - * Test get_author_id() when an error creating the user is encountered. - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_author_id_error() { - - // Error during creation. - $handler = function( $data ) { - $data['user_login'] = ''; - return $data; - }; - add_filter( 'llms_generator_new_author_data', $handler ); - $this->setExpectedException( Exception::class, 'Cannot create a user with an empty login name.', 1002 ); - LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( 'email' => 'fake@test.tld' ) ) ); - remove_filter( 'llms_generator_new_author_data', $handler ); - - } - - /** - * Test get_author_id_from_raw() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_author_id_from_raw() { - - $user = $this->factory->user->create(); - - // Retrievable from raw. - $this->assertEquals( $user, $this->stub->get_author_id_from_raw( array( 'author' => array( 'id' => $user ) ) ) ); - - // No raw submitted & no fallback, use current user. - wp_set_current_user( $user ); - $this->assertEquals( $user, $this->stub->get_author_id_from_raw( array() ) ); - - // Use fallback id. - $this->assertEquals( 832, $this->stub->get_author_id_from_raw( array(), 832 ) ); - - } - - /** - * Test default post status getter & setter - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_set_default_post_status() { - - // Default. - $this->assertEquals( 'draft', $this->stub->get_default_post_status() ); - - // Modify. - $this->stub->set_default_post_status( 'publish' ); - $this->assertEquals( 'publish', $this->stub->get_default_post_status() ); - - } - - /** - * Test get_term_id() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_term_id() { - - $name = 'mock generator term'; - $tax = 'course_cat'; - - // Create a term that doesn't already exist. - $id = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( $name, $tax ) ); - - $term = get_term_by( 'id', $id, $tax ); - $this->assertTrue( is_numeric( $id ) ); - $this->assertEquals( $name, $term->name ); - - // Already exists. - $this->assertEquals( $id, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( $name, $tax ) ) ); - - } - - /** - * Test get_term_id() when an error is encountered during creation of a new term - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_term_id_error() { - - $handler = function( $term ) { - return new WP_Error( 'mock-term-insert-err', 'Error' ); - }; - add_filter( 'pre_insert_term', $handler ); - $this->setExpectedException( Exception::class, 'Error creating new term "mock gen term".', 1001 ); - LLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( 'mock gen term', 'course_cat' ) ); - remove_filter( 'pre_insert_term', $handler ); - - } - - /** - * Test handle_reusable_blocks() when importing is disabled - * - * @since 4.7.0 - * - * @return void - */ - public function test_handle_reusable_blocks_disabled() { - - add_filter( 'llms_generator_is_reusable_block_importing_enabled', '__return_false' ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, 2 ) ) ); - remove_filter( 'llms_generator_is_reusable_block_importing_enabled', '__return_false' ); - - } - - /** - * Test handle_reusable_blocks() when no blocks to import - * - * @since 4.7.0 - * - * @return void - */ - public function test_handle_reusable_blocks_none() { - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array() ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array( '_extras' => array() ) ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array( '_extras' => array( 'blocks' => array() ) ) ) ) ); - - } - - /** - * Test handle_reusable_blocks() when no blocks to import - * - * @since 4.7.0 - * - * @return void - */ - public function test_handle_reusable_blocks() { - - $html = serialize_blocks( array( - array( - 'blockName' => 'core/block', - 'innerContent' => array( '' ), - 'attrs' => array( - 'ref' => 123, - ) - ), - array( - 'blockName' => 'core/paragraph', - 'innerContent' => array( 'Lorem ipsum dolor sit.' ), - 'attrs' => array(), - ), - array( - 'blockName' => 'core/block', - 'innerContent' => array( '' ), - 'attrs' => array( - 'ref' => 456, - ) - ), - ) ); - - $course_id = $this->factory->post->create( array( - 'post_content' => $html, - 'post_type' => 'course', - ) ); - $course = llms_get_post( $course_id ); - - $raw = array( - '_extras' => array( - 'blocks' => array( - '123' => array( - 'title' => 'Mock Block 1', - 'content' => 'mock content 1' - ), - '456' => array( - 'title' => 'Mock Block 2', - 'content' => 'mock content 2' - ), - ), - ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( $course, $raw ) ); - - // Proper return. - $this->assertTrue( $res ); - - // Post content updated with newly created blocks. - $block = parse_blocks( llms_get_post( $course_id )->get( 'content', true ) ); - - $this->assertEquals( 'core/block', $block[0]['blockName'] ); - $this->assertNotEquals( 123, $block[0]['attrs']['ref'] ); - $block1 = get_post( $block[0]['attrs']['ref'] ); - $this->assertEquals( 'Mock Block 1', $block1->post_title ); - $this->assertEquals( 'mock content 1', $block1->post_content ); - - $this->assertEquals( 'core/paragraph', $block[1]['blockName'] ); - - $this->assertEquals( 'core/block', $block[2]['blockName'] ); - $this->assertNotEquals( 456, $block[2]['attrs']['ref'] ); - $block2 = get_post( $block[2]['attrs']['ref'] ); - $this->assertEquals( 'Mock Block 2', $block2->post_title ); - $this->assertEquals( 'mock content 2', $block2->post_content ); - - } - - /** - * Test is_image_sideloading_enabled() - * - * @since 4.7.0 - * - * @return void - */ - public function test_is_image_sideloading_enabled() { - $this->assertTrue( $this->stub->is_image_sideloading_enabled() ); - } - - /** - * Test is_reusable_block_importing_enabled() - * - * @since 4.7.0 - * - * @return void - */ - public function test_is_reusable_block_importing_enabled() { - $this->assertTrue( $this->stub->is_reusable_block_importing_enabled() ); - } - - /** - * Test set_featured_image() - * - * @since 4.7.0 - * - * @return void - */ - public function test_set_featured_image() { - - $tests = array( - // String. - 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg', - // Parse from raw. - array( 'featured_image' => 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg' ), - ); - - foreach ( $tests as $arg ) { - - $post_id = $this->factory->post->create(); - $id = LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( $arg, $post_id ) ); - - $this->assertTrue( is_numeric( $id ) ); - $this->assertEquals( $id, get_post_thumbnail_id( $post_id ) ); - - } - - // No image. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( array(), $post_id ) ) ); - - // Error. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( 'fake', $post_id ) ) ); - - // Disabled. - add_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( 'fake', $post_id ) ) ); - remove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - - } - - /** - * Test sideload_image() - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_image() { - - $post = $this->factory->post->create(); - $url = 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg'; - - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) ); - - $this->assertStringNotContains( 'raw.githubusercontent', $res ); - $this->assertStringContains( 'christian-fregnan-unsplash', $res ); - - // Image already sideloaded so it's not sideloaded again. - $res2 = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) ); - $this->assertEquals( $res, $res2 ); - - // Test ID return. - $id = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url, 'id' ) ); - $this->assertTrue( is_numeric( $id ) ); - $this->assertEquals( $res2, wp_get_attachment_url( $id ) ); - - } - - /** - * Test sideload_image() error - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_image_error() { - - $post = $this->factory->post->create(); - $url = 'fake.jpg'; - - $res = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'http_request_failed', $res ); - - } - - /** - * Test sideload_images() - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_images() { - - $course = llms_get_post( $this->factory->post->create( array( - 'post_type' => 'course', - 'post_content' => '<!-- wp:image {"id":552,"sizeSlug":"large"} --> -<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> -<!-- /wp:image --> - -<!-- wp:gallery {"ids":[552,11]} --> -<figure class="wp-block-gallery columns-2 is-cropped"><ul class="blocks-gallery-grid"> -<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> -<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> -<!-- /wp:gallery --> - -<img src="https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg" alt="" class="wp-image-552"/>' - ) ) ); - - $raw = array( - '_extras' => array( - 'images' => array( - 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg', - 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/richard-i49WGMPd5aA-unsplash.jpg', - ), - ), - ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, $raw ) ) ); - $this->assertStringNotContains( 'raw.githubusercontent', $course->post->post_content ); - - } - - /** - * Test sideload_images(): skip sideloading of images from the same site. - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_images_from_same_site() { - - $course = llms_get_post( $this->factory->post->create( array( - 'post_type' => 'course', - 'post_content' => '<img src="https://example.org/fake-image.png" />', - ) ) ); - - $raw = array( - '_extras' => array( - 'images' => array( - 'https://example.org/fake-image.png', - ), - ), - ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, $raw ) ) ); - $this->assertEquals( '<img src="https://example.org/fake-image.png" />', $course->post->post_content ); - - - } - - /** - * Test sideload_images() with no images in post content - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_images_none() { - - $course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array() ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array( '_extras' => array() ) ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array( '_extras' => array( 'images' => array() ) ) ) ) ); - - } - - /** - * Test sideload_images() with sideloading disabled - * - * @since 4.7.0 - * - * @return void - */ - public function test_sideload_images_disabled() { - - $course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - - add_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array() ) ) ); - remove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-integration.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-integration.php deleted file mode 100644 index 2bdd531e19..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-integration.php +++ /dev/null @@ -1,317 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Integration class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group integrations - * - * @since 3.19.0 - */ -class LLMS_Test_Abstract_Integration extends LLMS_UnitTestCase { - - /** - * Retrieve the abstract class mock stub - * - * @since 3.19.0 - * @since 4.21.0 Use an anonymous class in favor of a mock abstract. - * - * @return LLMS_Abstract_Integration - */ - private function get_stub() { - - return new class() extends LLMS_Abstract_Integration { - - protected function configure() { - $this->id = 'mocker'; - $this->title = 'Mock Integration'; - $this->description = 'this is a mock description of the integration'; - do_action( 'llms_tests_mock_integration_configured' ); - } - - public $__is_installed = true; - public function is_installed() { - return $this->__is_installed; - } - - }; - - } - - /** - * Test the constructor. - * - * @since 4.21.0 - * - * @return void - */ - public function test_constructor() { - - $stub = $this->get_stub(); - - $configure_action = did_action( 'llms_tests_mock_integration_configured' ); - $init_action = did_action( 'llms_integration_mocker_init' ); - - remove_filter( 'lifterlms_integrations_settings_mocker', array( $stub, 'add_settings' ), 20 ); - - LLMS_Unit_Test_Util::set_private_property( $stub, 'plugin_basename', 'mockerpluginbasename' ); - - $stub->__construct(); - - // Actions ran. - $this->assertEquals( ++$configure_action, did_action( 'llms_tests_mock_integration_configured' ) ); - $this->assertEquals( ++$init_action, did_action( 'llms_integration_mocker_init' ) ); - - // Filter added. - $this->assertEquals( 20, has_filter( 'lifterlms_integrations_settings_mocker', array( $stub, 'add_settings' ) ) ); - - // Plugin actions link added. - $this->assertEquals( 100, has_action( 'plugin_action_links_mockerpluginbasename', array( $stub, 'plugin_action_links' ) ) ); - - - } - - /** - * Test add_settings() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_add_settings() { - - $stub = $this->get_stub(); - - // Must be an array. - $this->assertTrue( is_array( $stub->add_settings( array() ) ) ); - - // Only the default integration settings. - $this->assertEquals( 4, count( $stub->add_settings( array() ) ) ); - - // Mimic other settings from other integrations. - $this->assertEquals( 10, count( $stub->add_settings( array( 1, 2, 3, 4, 5, 6 ) ) ) ); - - } - - /** - * Test the get_option() method v1 behavior - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_v1() { - - $stub = $this->get_stub(); - $this->assertEquals( '', $stub->get_option( 'enabled' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) ); - $this->assertEquals( 'no', $stub->get_option( 'enabled', 'no' ) ); - $this->assertEquals( 'fake', $stub->get_option( 'enabled', 'fake' ) ); - - $stub->set_option( 'enabled', 'yes' ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'no' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'fake' ) ); - - } - - /** - * Test the get_option() method v2 behavior - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_v2() { - - $stub = $this->get_stub(); - LLMS_Unit_Test_Util::set_private_property( $stub, 'version', 2 ); - - $this->assertEquals( 'no', $stub->get_option( 'enabled' ) ); - - // Don't autoload the default value when a default value is passed. - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) ); - $this->assertEquals( 'no', $stub->get_option( 'enabled', 'no' ) ); - $this->assertEquals( 'fake', $stub->get_option( 'enabled', 'fake' ) ); - - $stub->set_option( 'enabled', 'yes' ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'no' ) ); - $this->assertEquals( 'yes', $stub->get_option( 'enabled', 'fake' ) ); - - } - - /** - * Directly test the get_option_default_value() method. - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_default_value() { - - $stub = $this->get_stub(); - - $this->assertEquals( 'no', $stub->get_option_default_value( '', $stub->get_option_name( 'enabled' ), false ) ); - $this->assertEquals( 'no', $stub->get_option_default_value( 'yes', $stub->get_option_name( 'enabled' ), false ) ); - - // Default value explicitly passed. - $this->assertEquals( 'yes', $stub->get_option_default_value( 'yes', $stub->get_option_name( 'enabled' ), true ) ); - - } - - - /** - * Test the get_priority() method - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_priority() { - - $stub = $this->get_stub(); - - // Default. - $this->assertEquals( 20, $stub->get_priority() ); - - // Redefined. - LLMS_Unit_Test_Util::set_private_property( $stub, 'priority', 50 ); - $this->assertEquals( 50, $stub->get_priority() ); - - } - - /** - * Test get_settings() - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_settings() { - - $stub = $this->get_stub(); - - $settings = LLMS_Unit_Test_Util::call_method( $stub, 'get_settings' ); - - $expected = array( - 'llms_integration_mocker_start', - 'llms_integration_mocker_title', - 'llms_integration_mocker_enabled', - 'llms_integration_mocker_end', - ); - $this->assertEquals( $expected, wp_list_pluck( $settings, 'id' ) ); - - } - - /** - * Test get_settings() when missing requirements. - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_settings_not_installed() { - - $stub = $this->get_stub(); - $stub->__is_installed = false; - - LLMS_Unit_Test_Util::set_private_property( $stub, 'description_missing', 'Missing requirements' ); - - $settings = LLMS_Unit_Test_Util::call_method( $stub, 'get_settings' ); - - $expected = array( - 'llms_integration_mocker_start', - 'llms_integration_mocker_title', - 'llms_integration_mocker_enabled', - 'llms_integration_mocker_missing_requirements_desc', - 'llms_integration_mocker_end', - ); - $this->assertEquals( $expected, wp_list_pluck( $settings, 'id' ) ); - - } - - /** - * Test is_available() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_is_available() { - - $stub = $this->get_stub(); - - // By default it is not available. - $this->assertFalse( $stub->is_available() ); - - // Enable it. - $stub->set_option( 'enabled', 'yes' ); - $this->assertTrue( $stub->is_available() ); - - // Explicitly disable it. - $stub->set_option( 'enabled', 'no' ); - $this->assertFalse( $stub->is_available() ); - - } - - /** - * Test is_enabled() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_is_enabled() { - - $stub = $this->get_stub(); - - // Disabled by default (no option found). - $this->assertFalse( $stub->is_enabled() ); - - // Enable it. - $stub->set_option( 'enabled', 'yes' ); - $this->assertTrue( $stub->is_enabled() ); - - // Explicitly disable it. - $stub->set_option( 'enabled', 'no' ); - $this->assertFalse( $stub->is_enabled() ); - - } - - /** - * Test is_installed() method - * - * By default this just returns true, extending classes override it - * - * @since 3.19.0 - * - * @return void - */ - public function test_is_installed() { - - $this->assertTrue( $this->get_stub()->is_installed() ); - - } - - /** - * Test plugin_action_links() - * - * @since 4.21.0 - * - * @return void - */ - public function test_plugin_action_links() { - - $mock_links = array( '<a href="#">FAKE</a>' ); - $expected_link = array( '<a href="http://example.org/wp-admin/admin.php?page=llms-settings&tab=integrations&section=mocker">Settings</a>' ); - $stub = $this->get_stub(); - - $this->assertEquals( array_merge( $mock_links, $expected_link ), $stub->plugin_action_links( $mock_links, 'mock', array(), 'all' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-options-data.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-options-data.php deleted file mode 100644 index 0d6d93c3e0..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-options-data.php +++ /dev/null @@ -1,155 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Integration class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group options - * @group settings - * - * @since 3.19.0 - * @since 4.21.0 Replaced the `get_stub()` method with `$this->main`, initialized in `set_up()`. - */ -class LLMS_Test_Abstract_Options_Data extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 4.21.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Options_Data' ); - } - - /** - * Test get_option(): version 1 behavior. - * - * @since 3.19.0 - * - * @return void - */ - public function test_get_option() { - - // Default value. - $this->assertEquals( '', $this->main->get_option( 'mock_option' ) ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - update_option( 'llms_mock_option', 'mockvalue' ); - - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option' ) ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'anothermockvalue' ) ); - - } - - /** - * Test get_option() when there's an empty string value explicitly saved in the database - * - * This test illustrates what's actually a bug but exists as expected behavior. Fixing this bug - * might result in unexpected consequences throughout add-ons utilizing the existing behavior as - * if it were intended and not a bug. - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_v1_expected_bug() { - - // An empty string value is expected here but due to the bug the supplied default value is supplied instead. - update_option( 'llms_mock_option', '' ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - // Option Does not exist so we should get the default value either way. - delete_option( 'llms_mock_option' ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - } - - /** - * Test get_option(): v2 behavior - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_v2_behavior() { - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'version', 2 ); - - // No default passed. - $this->assertEquals( '', $this->main->get_option( 'mock_option' ) ); - - // Default value passed. - $this->assertEquals( '', $this->main->get_option( 'mock_option', '' ) ); - $this->assertEquals( false, $this->main->get_option( 'mock_option', false ) ); - $this->assertEquals( array(), $this->main->get_option( 'mock_option', array() ) ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - update_option( 'llms_mock_option', 'mockvalue' ); - - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option' ) ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', '' ) ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'anothermockvalue' ) ); - - } - - /** - * Run test_get_option_v1_expected_bug() on v2 to see the bug fixed. - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_option_v2_expected_bug_fixed() { - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'version', 2 ); - - // This fails on v1, see `test_get_option_v1_expected_bug()`. - update_option( 'llms_mock_option', '' ); - $this->assertEquals( '', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - // Option Does not exist so we should get the default value. - delete_option( 'llms_mock_option' ); - $this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) ); - - } - - /** - * test get_option_name() method - * - * @since 3.19.0 - * @since 4.21.0 Use unit test utils to update private property value. - * - * @return void - */ - public function test_get_option_name() { - - $this->assertEquals( 'llms_mock_option', $this->main->get_option_name( 'mock_option' ) ); - - // Change the option prefix as an extending class might via overriding the `get_option_prefix()` method - LLMS_Unit_Test_Util::set_private_property( $this->main, 'option_prefix', 'llms_extended_' ); - - $this->assertEquals( 'llms_extended_mock_option', $this->main->get_option_name( 'mock_option' ) ); - - } - - /** - * test set_option() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_set_option() { - - delete_option( 'llms_mock_option' ); - $this->assertEquals( true, $this->main->set_option( 'mock_option', 'mockvalue' ) ); - $this->assertEquals( 'mockvalue', get_option( 'llms_mock_option', 'mockvalue' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-payment-gateway.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-payment-gateway.php deleted file mode 100644 index ec26f6e751..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-payment-gateway.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php -/** - * Tests for the LLMS_Payment_Gateway abstract - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group payment_gateway - * - * @since 5.3.0 - */ -class LLMS_Test_Payment_Gateway extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = $this->getMockForAbstractClass( 'LLMS_Payment_Gateway' ); - $this->main->id = 'cash-now'; - - } - - /** - * Test get_option_name() - * - * Tests options-related methods: - * + get_option() - * + get_option_default_value() - * + get_option_prefix() - * + get_option_name() - * + and set_option() - * - * @since 5.3.0 - * - * @return void - */ - public function test_option_methods() { - - $expected_name = 'llms_gateway_cash-now_title'; - $secure_key = 'LLMS_GATEWAY_CASH_NOW_TITLE'; - $expected_val = 'Cash Now'; - $this->assertEquals( $expected_name, $this->main->get_option_name( 'title' ) ); - - // Empty. - $this->assertEquals( '', $this->main->get_option( 'title') ); - - // Default value. - $this->main->title = 'Currency Immediately'; - $this->assertEquals( 'Currency Immediately', $this->main->get_option( 'title') ); - - // Set the title via WP core methods. - update_option( $expected_name, $expected_val ); - - $this->assertEquals( $expected_val, $this->main->get_option( 'title' ) ); - - // Secure not defined, fallsback with the default value. - $this->assertEquals( $expected_val, $this->main->get_option( 'title', $secure_key ) ); - - // Change the value via setter. - $this->main->set_option( 'title', 'Money Later' ); - $this->assertEquals( 'Money Later', $this->main->get_option( 'title' ) ); - - // Secure value defined. - define( $secure_key, 'Bucks Yesterday' ); - $this->assertEquals( 'Bucks Yesterday', $this->main->get_option( 'title', $secure_key ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-post-model.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-post-model.php deleted file mode 100644 index 871a64a4bb..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-post-model.php +++ /dev/null @@ -1,317 +0,0 @@ -<?php -/** - * Tests for the LLMS_Post_Model abstract - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group post_model_abstract - * @group post_models - * - * @since 4.10.0 - */ -class LLMS_Test_Abstract_Post_Model extends LLMS_UnitTestCase { - - private $post_type = 'mock_post_type'; - - /** - * @since 4.10.0 - * @var LLMS_Post_Model - */ - protected $stub; - - /** - * Setup before class. - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - register_post_type( 'mock_post_type' ); - - } - - /** - * Teradown after class. - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `tearDownAfterClass()` for compat with WP core changes. - * - * @return void - */ - public static function tear_down_after_class() { - - parent::tear_down_after_class(); - unregister_post_type( 'mock_post_type' ); - - } - - /** - * Setup the test case - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->stub = $this->get_stub(); - - } - - /** - * Retrieve the abstract class mock stub - * - * @since 4.10.0 - * - * @return LLMS_Post_Model - */ - private function get_stub() { - - $post = $this->factory->post->create_and_get( array( 'post_type' => $this->post_type ) ); - $stub = $this->getMockForAbstractClass( 'LLMS_Post_Model', array( $post ) ); - - LLMS_Unit_Test_Util::set_private_property( $stub, 'db_post_type', $this->post_type ); - LLMS_Unit_Test_Util::set_private_property( $stub, 'model_post_type', $this->post_type ); - - return $stub; - - } - - /** - * Test get() to ensure properties that should not be scrubbed are not scrubbed. - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_skipped_no_scrub_properties() { - - $tests = array( - 'content' => "<p>has html</p>\n", - 'name' => 'اسم-آخر', // See https://github.com/gocodebox/lifterlms/pull/1408. - ); - - // Filters should - foreach ( $tests as $key => $val ) { - - $this->stub->set( $key, $val ); - - // The scrub filter should not run when getting the value. - $actions = did_action( "llms_scrub_{$this->post_type}_field_{$key}" ); - - // Characters should not be scrubbed. - $this->assertEquals( 'name' === $key ? utf8_uri_encode( $val ) : $val, $this->stub->get( $key ) ); - - $this->assertSame( $actions, did_action( "llms_scrub_{$this->post_type}_field_{$key}" ) ); - - } - - } - - /** - * Test scrub_field(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_scrub_field() { - - $types = array( - 'absint' => array( - array( 1, 1 ), - array( 0, 0 ), - array( -1, 1 ), - array( 1.5, 1 ), - array( 2910, 2910 ), - array( '932', 932 ), - array( '34920.23', 34920 ), - array( 'string', 0 ), - array( '', 0 ), - array( null, 0 ), - ), - 'array' => array( - array( '', array() ), - array( 1, array( 1 ) ), - array( array( 1, 2, 3 ), array( 1, 2, 3 ) ), - array( array( 'test' ), array( 'test' ) ), - ), - 'boolean' => array( - array( true, true ), - array( false, false ), - array( 1, true ), - array( 0, false ), - array( null, false ), - ), - 'float' => array( - array( 1.0, 1.0 ), - array( 1, 1.0 ), - array( 0.234, 0.234 ), - array( 0, 0.0 ), - array( '2.230', 2.23 ), - array( null, 0.0 ), - ), - 'int' => array( - array( 1, 1 ), - array( 0, 0 ), - array( -1, -1 ), - array( 1.5, 1 ), - array( 2910, 2910 ), - array( '-932', -932 ), - array( '34920.23', 34920 ), - array( 'string', 0 ), - array( '', 0 ), - array( null, 0 ), - ), - 'yesno' => array( - array( 'yes', 'yes' ), - array( 'no', 'no' ), - array( 0, 'no' ), - array( 999, 'no' ), - array( false, 'no' ), - array( true, 'no' ), - array( null, 'no' ), - ), - 'text' => array( - array( 'yes', 'yes' ), - array( 'a text string.', 'a text string.' ), - array( 'no <b>tags</b>', 'no tags' ), - array( '', '' ), - array( null, '' ), - ), - 'html' => array( - array( 'yes', 'yes' ), - array( 'a text string.', 'a text string.' ), - array( 'Tags <b>are (mostly) okay</b>.', 'Tags <b>are (mostly) okay</b>.' ), - array( '', '' ), - array( null, '' ), - ), - ); - - $types['bool'] = $types['boolean']; - $types['string'] = $types['text']; - - foreach ( $types as $type => $tests ) { - - foreach ( $tests as $test ) { - - list( $input, $expected ) = $test; - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->stub, 'scrub_field', array( $input, $type ) ) ); - - } - } - - } - - /** - * Test `set_bulk()` to ensure single quotes and double quotes are correctly slashed. - * - * @since 5.3.1 - * - * @return void - */ - public function test_set_bulk_quotes() { - - $content = 'Content with "Double" Quotes and \'Single\' Quotes'; - $excerpt = 'Excerpt with "Double" Quotes and \'Single\' Quotes'; - $title = 'Title with "Double" Quotes and \'Single\' Quotes'; - - # Test with KSES filters - $this->stub->set_bulk( array( - 'content' => $content, - 'excerpt' => $excerpt, - 'title' => $title, - ) ); - $saved_post = get_post( $this->stub->get( 'id' ) ); - $this->assertEquals( $content, $saved_post->post_content ); - $this->assertEquals( $excerpt, $saved_post->post_excerpt ); - $this->assertEquals( $title, $saved_post->post_title ); - - # Test without KSES filters - kses_remove_filters(); - $this->stub->set_bulk( array( - 'content' => $content, - 'excerpt' => $excerpt, - 'title' => $title, - ) ); - $saved_post = get_post( $this->stub->get( 'id' ) ); - $this->assertEquals( $content, $saved_post->post_content ); - $this->assertEquals( $excerpt, $saved_post->post_excerpt ); - $this->assertEquals( $title, $saved_post->post_title ); - } - - /** - * Test toArray() method. - * - * @since 5.4.1 - * - * @return void - */ - public function test_toArray() { - - // Add custom meta data. - update_post_meta( $this->stub->get( 'id' ), '_custom_meta', 'meta_value' ); - - // Generate the array. - $array = $this->stub->toArray(); - - // Make sure all expected properties are returned. - $this->assertEqualSets( array_merge( array_keys( $this->stub->get_properties() ), array( 'custom', 'id' ) ), array_keys( $array ) ); - - // Values in the array should match the values retrieved by the object getters. - foreach ( $array as $key => $val ) { - - if ( 'custom' === $key ) { - $expect = array( - '_custom_meta' => array( - 'meta_value', - ), - ); - } elseif ( in_array( $key, array( 'content', 'excerpt', 'title' ), true ) ) { - $key = "post_{$key}"; - $expect = $this->stub->post->$key; - } else { - $expect = $this->stub->get( $key ); - } - - $this->assertEquals( $expect, $val, $key ); - } - - } - - /** - * Test toArray() method when the author is expanded. - * - * @since 5.4.1 - * - * @return void - */ - public function test_toArray_expanded_author() { - - $data = array( - 'role' => 'editor', - 'first_name' => 'Jeffrey', - 'last_name' => 'Lebowski', - '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.", - ); - $user = $this->factory->user->create_and_get( $data ); - $this->stub->set( 'author', $user->ID ); - - unset( $data['role'] ); - $data['id'] = $user->ID; - $data['email'] = $user->user_email; - - // Generate the array. - $array = $this->stub->toArray(); - $this->assertEquals( $data, $array['author'] ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-data.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-data.php deleted file mode 100644 index 08ba8596fe..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-data.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Session_Data class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group sessions - * @group session_data - * - * @since 4.0.0 - */ -class LLMS_Test_Abstract_Session_Data extends LLMS_UnitTestCase { - - /** - * Setup test case - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Session_Data' ); - - } - - /** - * Test get, set, and magic methods. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_set_isset_unset() { - - $vals = array( - 1, true, 'yes', 'true', 'on', - false, 0, 'no', 'off', - array(), array( 'yes' ), array( 'yes' => 'okay' ), - 1234.56, '1234.56', - 25, '20389' - ); - - foreach ( $vals as $val ) { - - $key = sprintf( '%s_%s', uniqid(), microtime() ); - - // Var not set. - $this->assertFalse( isset( $this->main->$key ) ); - - // Default value get when var is not set. - $this->assertEquals( $val, $this->main->get( $key, $val ) ); - - // Set. - $this->assertEquals( $val, $this->main->set( $key, $val ) ); - $this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) ); - - // Var is set. - $this->assertTrue( isset( $this->main->$key ) ); - - // Reset. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true ); - unset( $this->main->$key ); - - // Magic set. - $this->assertEquals( $val, $this->main->set( $key, $val ) ); - $this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) ); - - // Var is set. - $this->assertTrue( isset( $this->main->$key ) ); - - // Get. - $this->assertEquals( $val, $this->main->get( $key ) ); - - // Magic Get. - $this->assertEquals( $val, $this->main->$key ); - - // Reset. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true ); - - // Unset. - unset( $this->main->$key ); - $this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) ); - - // Gone, should return the default value. - $this->assertEquals( 'deleted', $this->main->get( $key, 'deleted' ) ); - - } - - } - - // public function - - /** - * Test get_id() - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_id() { - - // Already set. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'id', 'fakeid' ); - $this->assertEquals( 'fakeid', $this->main->get_id() ); - - // Generate a new id. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'id', '' ); - $id = $this->main->get_id(); - $this->assertTrue( is_string( $this->main->get_id() ) ); - $this->assertEquals( 32, strlen( $this->main->get_id() ) ); - - } - - /** - * Test get_id() for logged in users. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_id_logged_in() { - - $uid = $this->factory->user->create(); - wp_set_current_user( $uid ); - - $this->assertEquals( $uid, $this->main->get_id() ); - - } - -} diff --git a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-database-handler.php b/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-database-handler.php deleted file mode 100644 index 9565ac1752..0000000000 --- a/tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-database-handler.php +++ /dev/null @@ -1,188 +0,0 @@ -<?php -/** - * Tests for the LLMS_Abstract_Session_Database_Handler class - * - * @package LifterLMS/Tests/Abstracts - * - * @group abstracts - * @group sessions - * @group session_database_handler - * - * @since 4.0.0 - */ -class LLMS_Test_Abstract_Session_Database_Handler extends LLMS_UnitTestCase { - - /** - * Setup test case - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_sessions" ); - - $this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Session_Database_Handler' ); - - } - - /** - * Test clean() when deleting only expired sessions. - * - * @since 4.0.0 - * - * @return void - */ - public function test_clean_expired_only() { - - $prefix = LLMS_Cache_Helper::get_prefix( 'llms_session_id' ); - - $active = $this->create_mock_session_data( 2 ); - $expired = $this->create_mock_session_data( 2, true ); - - // Return 2 deletions. - $this->assertEquals( 2, $this->main->clean() ); - - // Active sessions were not removed. - global $wpdb; - $remaining = array_map( 'absint', $wpdb->get_col( "SELECT id FROM {$wpdb->prefix}lifterlms_sessions" ) ); - $this->assertEqualSets( $active, $remaining ); - - // New prefix because the old one is invalidated. - $this->assertNotEquals( $prefix, LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) ); - - } - - /** - * Test clean() when deleting all sessions. - * - * @since 4.0.0 - * - * @return void - */ - public function test_clean_all() { - - $active = $this->create_mock_session_data( 2 ); - $expired = $this->create_mock_session_data( 2, true ); - - // Return 4 deletions. - $this->assertEquals( 4, $this->main->clean( false ) ); - - // No sessions remain. - global $wpdb; - $remaining = $wpdb->get_col( "SELECT id FROM {$wpdb->prefix}lifterlms_sessions" ); - $this->assertEquals( array(), $remaining ); - - } - - /** - * Test delete() - * - * @since 4.0.0 - * - * @return void - */ - public function test_delete() { - - $id = $this->create_mock_session_data( 1 )[0]; - - global $wpdb; - $session_id = $wpdb->get_col( "SELECT session_key FROM {$wpdb->prefix}lifterlms_sessions WHERE id = {$id};" )[0]; - - // Mock cached data data. - wp_cache_set( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $session_id, 'mock_data', 'llms_session_id' ); - - $this->assertTrue( $this->main->delete( $session_id ) ); - - $this->assertFalse( wp_cache_get( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $session_id, 'llms_session_id' ) ); - - - - } - - /** - * Test save() when there's not data to be saved - * - * @since 4.0.0 - * - * @return void - */ - public function test_save_is_clean() { - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true ); - $this->assertFalse( $this->main->save( time() + HOUR_IN_SECONDS ) ); - - } - - /** - * Test save() - * - * @since 4.0.0 - * - * @return void - */ - public function test_save() { - - $this->main->set( 'item', 'yes' ); - - // Saved to DB. - $this->assertTrue( $this->main->save( time() + HOUR_IN_SECONDS ) ); - - // Cache set. - $data = wp_cache_get( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $this->main->get_id(), 'llms_session_id' ); - $this->assertEquals( array( 'item' => 'yes' ), $data ); - - } - - /** - * Test read() when there's no saved data so it returns a default value - * - * @since 4.0.0 - * - * @return void - */ - public function test_read_default() { - - $this->assertEquals( 'defaultvalue', $this->main->read( 'fake', 'defaultvalue' ) ); - - } - - /** - * Test read() when there's a cache hit - * - * @since 4.0.0 - * - * @return void - */ - public function test_read_cache_hit() { - - wp_cache_set( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . 'fake_session', 'mock_data', 'llms_session_id' ); - $this->assertEquals( 'mock_data', $this->main->read( 'fake_session' ) ); - - } - - - /** - * Test read() when there's a cache miss - * - * @since 4.0.0 - * - * @return void - */ - public function test_read_cache_miss() { - - $this->main->set( 'something', 'is_set' ); - $this->main->save( time() + HOUR_IN_SECONDS ); - - LLMS_Cache_Helper::invalidate_group( 'llms_session_id' ); - - $this->assertEquals( array( 'something' => 'is_set' ), $this->main->read( $this->main->get_id() ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-assets.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-assets.php deleted file mode 100644 index 5a9b63562a..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-assets.php +++ /dev/null @@ -1,289 +0,0 @@ -<?php -/** - * Test Admin Assets Class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group admin_assets - * @group assets - * - * @since 4.3.3 - */ -class LLMS_Test_Admin_Assets extends LLMS_Unit_Test_Case { - - /** - * Setup the test case - * - * @since 4.3.3 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Admin_Assets(); - - } - - /** - * Tear down test case - * - * Dequeue & Dereqister all assets that may have been enqueued during tests. - * - * @since 4.3.3 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - /** - * List of asset handles that may have been enqueued or registered during the test - * - * We do not care if they actually were registered or enqueued, we'll remove them - * anyway since the functions will fail silently for assets that were not - * previously enqueued or registered. - */ - $handles = array( - 'llms-google-charts', - 'llms-analytics' - ); - - foreach ( $handles as $handle ) { - wp_dequeue_script( $handle ); - wp_deregister_script( $handle ); - } - - } - - /** - * Test get_analytics_options() - * - * @since 4.5.1 - * - * @return void - */ - public function test_get_analytics_options() { - - $this->assertEquals( array( 'currency_format' => '$#,##0.00' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) ); - - // Simulate comma decimal separator that's forced back to decimals. - add_filter( 'lifterlms_thousand_separator', function( $sep ) { return '.'; } ); - add_filter( 'lifterlms_decimal_separator', function( $sep ) { return ','; } ); - - $this->assertEquals( array( 'currency_format' => '$#,##0.00' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) ); - - remove_all_filters( 'lifterlms_thousand_separator' ); - remove_all_filters( 'lifterlms_decimal_separator' ); - - // Simulate non US symbol on the right with a space. - add_filter( 'lifterlms_currency_symbol', function( $sym ) { return 'A'; } ); - add_filter( 'lifterlms_price_format', function( $format ) { return '%2$s %1$s'; } ); - - $this->assertEquals( array( 'currency_format' => '#,##0.00 A' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) ); - - remove_all_filters( 'lifterlms_currency_symbol' ); - remove_all_filters( 'lifterlms_price_format' ); - - } - - /** - * Test maybe_enqueue_reporting() on a screen where it shouldn't be registered. - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_wrong_screen() { - - $screen = (object) array( 'base' => 'fake' ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetNotRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetNotRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetNotEnqueued( 'script', 'llms-google-charts' ); - $this->assertAssetNotEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on the general settings page where analytics are required for the data widgets - * - * This test tests the default "assumed" tab when there's no `tab` set in the $_GET array. - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_general_settings_assumed() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-settings' ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetIsEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on the general settings page where analytics are required for the data widgets - * - * This test is the same as test_maybe_enqueue_reporting_general_settings_assumed() except this one explicitly - * tests for the presence of the `tab=general` in the $_GET array. - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_general_settings_explicit() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-settings' ); - $this->mockGetRequest( array( 'tab' => 'general' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetIsEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on settings tabs other than general, scripts will be registered but not enqueued. - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_other_tabs() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-settings' ); - $this->mockGetRequest( array( 'tab' => 'fake' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetNotEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on reporting screens where the scripts aren't needed. - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_invalid_reporting_screens() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetNotEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on the enrollments reporting screen - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_enrollments_reporting_screens() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' ); - $this->mockGetRequest( array( 'tab' => 'enrollments' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetIsEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on the sales reporting screen - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_sales_reporting_screens() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' ); - $this->mockGetRequest( array( 'tab' => 'sales' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetIsEnqueued( 'script', 'llms-analytics' ); - - } - - /** - * Test maybe_enqueue_reporting() on the main quizzes reporting screen - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_quiz_main_reporting_screens() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' ); - $this->mockGetRequest( array( 'tab' => 'quizzes' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetNotEnqueued( 'script', 'llms-analytics' ); - $this->assertAssetNotEnqueued( 'script', 'llms-quiz-attempt-review' ); - - } - - /** - * Test maybe_enqueue_reporting() on the quiz attempts reporting screen - * - * @since 4.3.3 - * - * @return void - */ - public function test_maybe_enqueue_reporting_quiz_attempts_reporting_screens() { - - $screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' ); - $this->mockGetRequest( array( 'tab' => 'quizzes', 'stab' => 'attempts' ) ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) ); - - $this->assertAssetIsRegistered( 'script', 'llms-google-charts' ); - $this->assertAssetIsRegistered( 'script', 'llms-analytics' ); - - $this->assertAssetNotEnqueued( 'script', 'llms-analytics' ); - $this->assertAssetIsEnqueued( 'script', 'llms-quiz-attempt-review' ); - - } - - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-builder.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-builder.php deleted file mode 100644 index 41e38365d9..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-builder.php +++ /dev/null @@ -1,601 +0,0 @@ -<?php -/** - * Test Admin Builder API - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group builder - * - * @since 3.37.12 - * @since 4.14.0 Added tests on the autosave option. - * @since 4.16.0 Added tests on 'the_title' and 'the_content' filters not affecting the save. - * @since 5.1.3 Added tests on lesson moved into a brand new section. - */ -class LLMS_Test_Admin_Builder extends LLMS_Unit_Test_Case { - - /** - * Setup the test case - * - * @since 3.37.12 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->main = 'LLMS_Admin_Builder'; - } - - /** - * Test get_autosave_states() - * - * @since 4.14.0 - * - * @return void - */ - public function test_get_autosave_status() { - - // Defaults to yes. - $this->assertEquals( 'yes', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) ); - - // User has no value set. - $user = $this->factory->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $user ); - $this->assertEquals( 'yes', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) ); - - // Explicit yes. - update_user_meta( $user, 'llms_builder_autosave','yes' ); - $this->assertEquals( 'yes', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) ); - - // Explicit no. - update_user_meta( $user, 'llms_builder_autosave','no' ); - $this->assertEquals( 'no', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) ); - - } - - /** - * Test LLMS_Admin_Builder::get_existing_posts() with a lesson created by users of different roles. - * - * @since 5.8.0 - * - * @link https://github.com/gocodebox/lifterlms/issues/1849 - * - * @return void - * @throws ReflectionException - */ - public function test_get_existing_lesson_by_role() { - - $all_lesson_ids = array(); - $instructor_lesson_ids = array(); - $users = array(); - $roles = array( - 'administrator', - 'lms_manager', - 'instructor', - 'instructors_assistant', - 'student', - ); - - // Create multiple users for each role. - foreach ( $roles as $role ) { - - for ( $user_counter = 0; $user_counter < 2; $user_counter ++ ) { - - $user = $this->factory->user->create_and_get( array( 'role' => $role ) ); - $users[ $user->ID ] = $user; - - // Create multiple courses that are authored by this instructor. - if ( 'instructor' === $role ) { - wp_set_current_user( $user->ID ); - - if ( ! isset( $instructor_lesson_ids[ $user->ID ] ) ) { - $instructor_lesson_ids[ $user->ID ] = array(); - } - - for ( $course_counter = 0; $course_counter < 2; $course_counter ++ ) { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 2 ) ); - foreach ( $course->get_lessons( 'ids' ) as $lesson_id ) { - $all_lesson_ids[] = $lesson_id; - $instructor_lesson_ids[ $user->ID ][] = $lesson_id; - } - } - - // Create an instructor assistant for this instructor. - $assistant = $this->factory->instructor->create_and_get( array( 'role' => 'instructors_assistant' ) ); - $assistant->add_parent( $user->ID ); - $users[ $assistant->get_id() ] = $assistant->get_user(); - } - } - } - - // Test each user's capability to build courses with lessons. - foreach ( $users as $user_id => $user ) { - - wp_set_current_user( $user_id ); - $role = reset( $user->roles ); // We created users with only one role. - - // Get lessons that the user can access. - $lesson_search = LLMS_Unit_Test_Util::call_method( $this->main, 'get_existing_posts', array( 'lesson' ) ); - $found_lesson_ids = array(); - foreach ( $lesson_search['results'] as $result ) { - $found_lesson_ids[] = $result['id']; - } - - switch ( $role ) { - case 'administrator': - case 'lms_manager': - $message = "$role can build courses with all lessons."; - $this->assertEqualSets( $all_lesson_ids, $found_lesson_ids, $message ); - break; - case 'instructor': - $message = 'Instructors can build courses with lessons that they have authored.'; - $this->assertEqualSets( $instructor_lesson_ids[ $user_id ], $found_lesson_ids, $message ); - break; - case 'instructors_assistant': - $assistant = llms_get_instructor( $user_id ); - $instructor_ids = (array) $assistant->get( 'parent_instructors' ); - $expected_lesson_ids = $instructor_lesson_ids[ reset( $instructor_ids ) ] ?? array(); - $message = 'Instructor\'s assistants can build courses with lessons that their ' . - 'parent instructors have authored.'; - $this->assertEqualSets( $expected_lesson_ids, $found_lesson_ids, $message ); - break; - case 'student': - $this->assertEmpty( $found_lesson_ids, 'Students can not build courses with any lessons.' ); - break; - } - } - } - - /** - * Filter callback for `llms_builder_trash_custom_item` used to mock a custom item deletion. - * - * @since 3.37.12 - * - * @param null|array $trash_response Denotes the trash response. See description above for details. - * @param array $res The initial default error response which can be modified for your needs and then returned. - * @param mixed $id The ID of the course element. Usually a WP_Post id. - * @return array - */ - public function filter_llms_builder_trash_custom_item( $ret, $res, $id ) { - return compact( 'id' ); - } - - /** - * Test process_trash() for an invalid post id (one that doesn't exist). - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_invalid_post_id() { - - $data = array( - 'trash' => array( $this->factory->post->create() + 1 ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - $this->assertEquals( $data['trash'][0], $res[0]['id'] ); - $this->assertStringContains( 'Invalid ID.', $res[0]['error'] ); - - } - - /** - * Test process_trash() for a custom / 3rd party item. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_custom_item() { - - add_filter( 'llms_builder_trash_custom_item', array( $this, 'filter_llms_builder_trash_custom_item' ), 10, 3 ); - - $data = array( - 'trash' => array( $this->factory->post->create() + 1 ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - $this->assertEquals( array( 'id' => $data['trash'][0] ), $res[0] ); - - remove_filter( 'llms_builder_trash_custom_item', array( $this, 'filter_llms_builder_trash_custom_item' )); - - } - - /** - * Test process_trash() for an invalid post type. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_invalid_post_type() { - - $data = array( - 'trash' => array( $this->factory->post->create() ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - $this->assertEquals( $data['trash'][0], $res[0]['id'] ); - $this->assertEquals( 'Posts cannot be deleted via the Course Builder.', $res[0]['error'] ); - - } - - /** - * Test process_trash() for success when the post is force-deleted. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_force_delete_success() { - - $types = array( 'section', 'llms_question', 'llms_quiz' ); - foreach ( $types as $type ) { - - $post_id = $this->factory->post->create( array( 'post_type' => $type ) ); - - $data = array( - 'trash' => array( $post_id ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - // Proper return. - $this->assertEquals( array( 'id' => $post_id ), $res[0] ); - - // Post has been force deleted. - $this->assertNull( get_post( $post_id ) ); - - } - - } - - /** - * Test process_trash() when an error is encountered deleting the post. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_deletion_error() { - - // Mock the return of `wp_delete_post()` to simulate an error. - add_filter( 'pre_delete_post', '__return_false' ); - - $post_id = $this->factory->post->create( array( 'post_type' => 'section' ) ); - - $data = array( - 'trash' => array( $post_id ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - $this->assertEquals( $post_id, $res[0]['id'] ); - $this->assertStringContains( 'Error deleting the Section', $res[0]['error'] ); - - remove_filter( 'pre_delete_post', '__return_false' ); - - } - - /** - * Test process_trash() success when moving an item to the trash. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_move_to_trash() { - - $post_id = $this->factory->post->create( array( 'post_type' => 'lesson' ) ); - - $data = array( - 'trash' => array( $post_id ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - // Proper return. - $this->assertEquals( array( 'id' => $post_id ), $res[0] ); - - // Post has been trashed - $this->assertEquals( 'trash', get_post_status( $post_id ) ); - - } - - /** - * Test process_trash() when deleting a question choice. - * - * @since 3.37.12 - * - * @return void - */ - public function test_process_trash_question_choice() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) ); - $quiz = $course->get_lessons()[0]->get_quiz(); - $question = $quiz->get_questions()[0]; - $choice = $question->get_choices()[0]; - $choice_id = $choice->get( 'id' ); - - $id = sprintf( '%1$d:%2$s', $question->get( 'id' ), $choice_id ); - - $data = array( - 'trash' => array( $id ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) ); - - // Proper return. - $this->assertEquals( array( 'id' => $id ), $res[0] ); - - // Choice has been deleted. - $this->assertFalse( $question->get_choice( $choice_id ) ); - - } - - /** - * Test the ajax save an possible filters applied to the title and the content - * - * @since 4.16.0 - * - * @return void - */ - public function test_ajax_save_unfiltered_title_content() { - - // Handle wp die ajax and simulate ajax call. - add_filter( 'wp_die_ajax_handler', array( $this, '_wp_die_handler' ), 1 ); - add_filter( 'wp_doing_ajax', '__return_true' ); - - $user = $this->factory->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $user ); - - // Add title and content filters. - foreach ( array( 'the_title', 'the_content' ) as $filter_hook ) { - add_filter( $filter_hook, array( $this, '__return_filtered' ), 999999 ); - } - // Create a valid course. - $course = $this->factory->course->create( array( 0,0,0,0 ) ); - - $request = array( - 'action_type' => 'ajax_save', - 'course_id' => $course, - 'llms_builder' => array( - ), - ); - - $to_save = array( - 'updates' => array( - 'id' => $course, - 'sections' => array( - array( - 'id' => 'temp_28', - 'parent_course' => $course, - 'title' => 'New Section', - 'type' => 'section', - 'lessons' => array( - array( - 'id' => 'temp_40', - 'title' => 'New Lesson', - 'content' => '<p>Content</p>', - 'video_embed' => 'https://somevideo', - 'parent_course' => $course, - 'parent_section' => 'temp_28', - 'type' => 'lesson', - 'quiz' => array( - 'id' => 'temp_123', - 'title' => 'New Quiz', - 'type' => 'llms_quiz', - 'lesson_id' => 'temp_40', - 'content' => '<p>Quiz description</p>', - 'questions' => array( - array( - 'id' => 'temp_155', - 'content' => '<p>Question description 1</p>', - 'title' => 'Question title 1', - 'parent_id' => 'temp_123', - 'type' => 'llms_question', - 'question_type' => 'choice', - ), - array( - 'id' => 'temp_156', - 'content' => '<p>Question description 2</p>', - 'title' => 'Question title 2', - 'parent_id' => 'temp_123', - 'type' => 'llms_question', - 'question_type' => 'choice', - ), - ), - ), - ), - ), - ), - ), - ), - 'id' => $course, - ); - - $request['llms_builder'] = wp_json_encode( $to_save ); - - // Simulate the ajax save request. - ob_start(); - try { - LLMS_Unit_Test_Util::call_method( $this->main, 'handle_ajax', array( $request ) ); - } catch ( WPAjaxDieContinueException $e ) {} - $res = json_decode( $this->last_response, true ); - - // Check the request went through. - $this->assertEquals( 'success', $res['llms_builder']['status'] ); - - // Check the raw title and content have not been affected by the filters. - $this->check_title_content_filtering_on_save( $res, $to_save ); - - /* Check the raw title and content have not been affected by the filters. */ - - // Following the instructions contained in the handle_ajax method that actually perform the update, - // but without removing any filters on the_title, the_content. - $req = $request; - $req['llms_builder'] = stripslashes( $request['llms_builder'] ); - $res = LLMS_Unit_Test_Util::call_method( - $this->main, - 'heartbeat_received', - array( - array(), - $req, - ) - ); - - // Check the request went through. - $this->assertEquals( 'success', $res['llms_builder']['status'] ); - - // Check the raw title and content have not been affected by the filters. - $this->check_title_content_filtering_on_save( $res, $to_save ); - - // Reset. - foreach ( array( 'the_title', 'the_content' ) as $filter_hook ) { - remove_filter( $filter_hook, array( $this, '__return_filtered' ), 999999 ); - } - remove_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 ); - remove_filter( 'wp_doing_ajax', '__return_true' ); - } - - /** - * Helper that always returns the string '{filtered}' - * - * @since 4.16.0 - * - * @return string - */ - private function __return_filtered() { - return '{filtered}'; - } - - /** - * Helper to check whether the title and content props are filtered on save. - * - * @since 4.16.0 - * - * @param array $res Associative array containing the response from the save ajax method. - * @param array $sent Associative array containing the data sent for the update. - * @return void - */ - private function check_title_content_filtering_on_save( $res, $sent ) { - - $li = 0; - - foreach ( $res['llms_builder']['updates']['sections'][0]['lessons'] as $lesson ) { - $lq = 0; - foreach ( array( 'title', 'content' ) as $prop ) { - // Check lesson's title and content. - $this->assertStringContainsString( - $sent['updates']['sections'][0]['lessons'][$li][$prop], - llms_get_post( $lesson['id'] )->get( $prop, true ), - $prop - ); - $this->assertStringNotContainsString( - $this->__return_filtered(), - llms_get_post( $lesson['id'] )->get( $prop, true ), - $prop - ); - - // Check quiz title and content. - $this->assertStringContainsString( - $sent['updates']['sections'][0]['lessons'][$li]['quiz'][$prop], - llms_get_post( $lesson['quiz']['id'] )->get( $prop, true ), - $prop - ); - $this->assertStringNotContainsString( - $this->__return_filtered(), - llms_get_post( $lesson['quiz']['id'] )->get( $prop, true ), - $prop - ); - } - - foreach ( $lesson['quiz']['questions'] as $question ) { - foreach ( array( 'title', 'content' ) as $prop ) { - // Check question title and content. - $this->assertStringContainsString( - $sent['updates']['sections'][0]['lessons'][$li]['quiz']['questions'][$lq][$prop], - llms_get_post( $question['id'] )->get( $prop, true ), - $prop - ); - $this->assertStringNotContainsString( - $this->__return_filtered(), - llms_get_post( $question['id'] )->get( $prop, true ), - $prop - ); - } - $lq++; - } - $li++; - } - } - - /** - * Test a lesson is correctly "moved" into a brand new section :) - * - * @since 5.1.3 - * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`. - * Replaced the call to the deprecated `LLMS_Lesson::set_parent_course()` method with `LLMS_Lesson::set( 'parent_course', $course_id )`. - * - * @return void - */ - public function test_move_lesson_in_a_brand_new_section() { - - // Create a Course with a Lesson. - $course = $this->factory->course->create_and_get( array( - 'sections' => 1, - 'lessons' => 1, - 'quizzes' => 0, - ) ); - $lesson = $course->get_lessons()[0]; - - // Create a section. - $section_id = $this->factory->post->create( array( 'post_type' => 'section' ) ); - $section = llms_get_post( $section_id ); - // Add the section to the course above. - $section->set( 'parent_course', $course->get( 'id' ) ); - - // Simulate the course lesson moved from its section to the brand new one. - // Build builder data. - $lessons_data_from_builder = array( - array( - 'parent_section' => 'temp_108', // temp parent section. - 'id' => $lesson->get( 'id' ), - ), - ); - - LLMS_Unit_Test_Util::call_method( - $this->main, - 'update_lessons', - array( - $lessons_data_from_builder, - $section // The just created section parent. - ) - ); - - // Check lesson parents. - $this->assertEquals( $course->get( 'id' ), $lesson->get( 'parent_course' ) ); - $this->assertEquals( $section->get( 'id' ), $lesson->get_parent_section() ); - - } - - /** - * Catch wp_die() called by ajax methods & store the output buffer contents for use later. - * - * The same method is used in LLMS_Test_AJAX_Handler. - * @since 4.16.0 - * - * @param string $msg Die msg. - * @return void - */ - public function _wp_die_handler( $msg ) { - $this->last_response = ob_get_clean(); - throw new WPAjaxDieContinueException( $msg ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-import.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-import.php deleted file mode 100644 index 113b940c70..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-import.php +++ /dev/null @@ -1,549 +0,0 @@ -<?php -/** - * Tests for LLMS_Admin_Review class - * - * @package LifterLMS_Tests/Admin - * - * @group admin - * @group admin_import - * - * @since 3.35.0 - * @since 3.37.8 Update path to assets directory. - * @since 4.7.0 Test success message generation. - * @since 4.8.0 Move includes to `setUpBeforeClass()` method. - */ -class LLMS_Test_Admin_Import extends LLMS_UnitTestCase { - - /** - * Setup before class. - * - * @since 4.8.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.import.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php'; - - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php'; - - } - - /** - * Setup test case. - * - * @since 3.35.0 - * @since 4.8.0 Move includes to `set_up_before_class()` method. - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->import = new LLMS_Admin_Import(); - - } - - /** - * Tear down test case. - * - * @since 3.35.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - unset( $_FILES['llms_import'] ); - - } - - /** - * Mock a file upload for some test data. - * - * @since 3.35.0 - * - * @param int $err Mock a PHP file upload error code, see https://www.php.net/manual/en/features.file-upload.errors.php. - * @param string $import Filename to use for the import, see `import-*.json` files in the `tests/assets` directory. - * @return void - */ - private function mock_file_upload( $err = 0, $import = null ) { - - $file = is_null( $import ) ? LLMS_PLUGIN_DIR . 'sample-data/sample-course.json' : $import; - - $_FILES['llms_import'] = array( - 'name' => basename( $file ), - 'tmp_name' => $file, - 'type' => 'application/json', - 'error' => $err, - 'size' => filesize( $file ), - ); - - } - - /** - * Test the add_help_tabs() method. - * - * @since 4.8.0 - * - * @return void - */ - public function test_add_help_tabs() { - - // Not on the right screen. - $this->assertFalse( $this->import->add_help_tabs() ); - - // On the right screen. - llms_tests_mock_current_screen( 'lifterlms_page_llms-import' ); - - $screen = $this->import->add_help_tabs(); - - // Tab has been added. - $tab_id = 'llms_import_overview'; - $tab = $screen->get_help_tab( $tab_id ); - - $this->assertEquals( $tab_id, $tab['id'] ); - - // Has sidebar content. - $this->assertStringContains( 'Import Documentation', $screen->get_help_sidebar() ); - - llms_tests_reset_current_screen(); - - } - - /** - * Test cloud_import() errors from nonce - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_nonce() { - - // No nonce. - $this->assertFalse( $this->import->cloud_import() ); - - // Invalid nonce. - $this->mockPostRequest( array( - 'llms_cloud_importer_nonce' => 'fake', - ) ); - $this->assertFalse( $this->import->cloud_import() ); - - } - - /** - * Test cloud_import() user permission errors - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_permissions() { - - $this->mockPostRequest( array( - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - $this->assertFalse( $this->import->cloud_import() ); - - } - - /** - * Test cloud_import() missing necessary data - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_no_course_id() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - $res = $this->import->cloud_import(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms-cloud-import-missing-id', $res ); - - } - - /** - * Test cloud_import() with an api errors - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_api() { - - $handler = function( $preempt ) { - return new WP_Error( 'mocked', 'Mocked error.' ); - }; - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_cloud_import_course_id' => 1, - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - - add_filter( 'pre_http_request', $handler ); - - $res = $this->import->cloud_import(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'mocked', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test cloud_import() with a real API error from submitting invalid ids - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_api_real() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_cloud_import_course_id' => 1, - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - - $res = $this->import->cloud_import(); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'not-found', $res ); - - } - - /** - * Test cloud_import() with a generator error - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_error_generator() { - - $handler = function( $preempt ) { - return array( 'fake api response' ); - }; - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_cloud_import_course_id' => 1, - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - - add_filter( 'pre_http_request', $handler ); - - $res = $this->import->cloud_import(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'missing-generator', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test cloud_import() success - * - * @since 4.8.0 - * - * @return void - */ - public function test_cloud_import_success() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_cloud_import_course_id' => 33579, // Free Course Lead Magnet Template. - 'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ), - ) ); - - $this->assertTrue( $this->import->cloud_import() ); - - } - - /** - * Test enqueue() method - * - * @since 4.8.0 - * - * @return void - */ - public function test_enqueue() { - - $slug = 'llms-admin-importer'; - - $this->assertNull( $this->import->enqueue() ); - $this->assertAssetNotRegistered( 'style', $slug ); - $this->assertAssetNotEnqueued( 'style', $slug ); - - llms_tests_mock_current_screen( 'lifterlms_page_llms-import' ); - - $this->assertTrue( $this->import->enqueue() ); - - $this->assertAssetIsRegistered( 'style', $slug ); - $this->assertAssetIsEnqueued( 'style', $slug ); - - llms_tests_reset_current_screen(); - - } - - /** - * Test get_screen() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_screen() { - - // Wrong screen. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' ) ); - - llms_tests_mock_current_screen( 'admin.php' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' ) ); - - // Right screen. - llms_tests_mock_current_screen( 'lifterlms_page_llms-import' ); - $screen = LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' ); - $this->assertTrue( $screen instanceof WP_Screen ); - $this->assertEquals( 'lifterlms_page_llms-import', $screen->id ); - - } - - /** - * Test get_success_message() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_success_message() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $generator = new LLMS_Generator( array() ); - $course = $this->factory->post->create_many( 2, array( 'post_type' => 'course' ) ); - $user = $this->factory->user->create_many( 1 ); - LLMS_Unit_Test_Util::set_private_property( $generator, 'generated', compact( 'course', 'user' ) ); - - $res = LLMS_Unit_Test_Util::call_method( $this->import, 'get_success_message', array( $generator ) ); - - $this->assertStringContains( 'Import Successful!', $res ); - - foreach( $course as $id ) { - $this->assertStringContains( esc_url( get_edit_post_link( $id ) ), $res ); - $this->assertStringContains( get_the_title( $id ), $res ); - } - - $user = new WP_User( $user[0] ); - $this->assertStringContains( esc_url( get_edit_user_link( $user->ID ) ), $res ); - $this->assertStringContains( $user->display_name, $res ); - - } - - /** - * Upload form not submitted. - * - * @since 3.35.0 - * - * @return [type] - */ - public function test_import_not_submitted() { - - $this->assertFalse( $this->import->upload_import() ); - - } - - /** - * Submitted with an invalid nonce. - * - * @since 3.35.0 - * - * @return void - */ - public function test_upload_import_invalid_nonce() { - - $this->mockPostRequest( array( - 'llms_importer_nonce' => 'fake', - ) ); - $this->assertFalse( $this->import->upload_import() ); - - } - - /** - * Submitted without files. - * - * @since 3.35.0 - * - * @return void - */ - public function test_upload_import_missing_files() { - - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - $this->assertFalse( $this->import->upload_import() ); - - } - - /** - * Submitted by a user without proper permissions. - * - * @since 3.35.0 - * - * @return void - */ - public function test_upload_import_invalid_permissions() { - - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - $this->mock_file_upload(); - $this->assertFalse( $this->import->upload_import() ); - - - } - - /** - * File encountered validation errors. - * - * @since 3.35.0 - * - * @return void - */ - public function test_upload_import_validation_issues() { - - wp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - - // Test all the possible PHP file errors. - $errs = array( - 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', - 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', - 3 => 'The uploaded file was only partially uploaded.', - 4 => 'No file was uploaded.', - 6 => 'Missing a temporary folder.', - 7 => 'Failed to write file to disk.', - 8 => 'File upload stopped by extension.', - 9 => 'Unknown upload error.', - ); - foreach ( $errs as $i => $msg ) { - - $this->mock_file_upload( $i ); - $err = $this->import->upload_import(); - $this->assertIsWPError( $err ); - $this->assertWPErrorMessageEquals( $msg, $err ); - - } - - // invalid filetype. - $this->mock_file_upload(); - $_FILES['llms_import']['name'] = 'mock.txt'; - - $err = $this->import->upload_import(); - $this->assertIsWPError( $err ); - $this->assertWPErrorMessageEquals( 'Only valid JSON files can be imported.', $err ); - - } - - /** - * Generator encountered an issues when setting the generator method. - * - * @since 3.35.0 - * @since 3.37.8 Update path to assets directory. - * - * @return void - */ - public function test_upload_import_invalid_generator_error() { - - wp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - - global $lifterlms_tests; - $this->mock_file_upload( 0, $lifterlms_tests->assets_dir . 'import-fake-generator.json' ); - - $err = $this->import->upload_import(); - $this->assertIsWPError( $err ); - $this->assertWPErrorCodeEquals( 'invalid-generator', $err ); - - } - - /** - * Error during generation (missing required data) - * - * @since 3.35.0 - * @since 3.37.8 Update path to assets directory. - * @since 4.9.0 PHP8 upgrades from notice to warning. - * - * @return void - */ - public function test_upload_import_generation_error() { - - wp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - - global $lifterlms_tests; - $this->mock_file_upload( 0, $lifterlms_tests->assets_dir . 'import-error.json' ); - - $err = $this->import->upload_import(); - $this->assertIsWPError( $err ); - - $expected_code = 8 === PHP_MAJOR_VERSION ? 'E_WARNING' : 'E_NOTICE'; - $this->assertWPErrorCodeEquals( $expected_code, $err ); - - } - - /** - * Success. - * - * @since 3.35.0 - * - * @return void - */ - public function test_upload_import_success() { - - wp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ), - ) ); - $this->mock_file_upload(); - - $this->assertTrue( $this->import->upload_import() ); - - } - - /** - * Test output() method - * - * @since 4.7.0 - * - * @return void - */ - public function test_output() { - - $this->assertOutputContains( '<div class="wrap lifterlms llms-import-export">', array( $this->import, 'output' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-menus.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-menus.php deleted file mode 100644 index 6b4033ddc4..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-menus.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php -/** - * Test Admin Menus Class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group admin_menus - * - * @since 4.7.0 - */ -class LLMS_Test_Admin_Menus extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/class.llms.admin.reporting.php'; - require_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.menus.php'; - } - - /** - * Setup the test case. - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Admin_Menus(); - - } - - /** - * Test reporting_page_init() when there's permission issues. - * - * @since 4.7.0 - * - * @return void - */ - public function test_reporting_page_init_permissions_error() { - - $this->mockGetRequest( array( 'student_id' => $this->factory->student->create() ) ); - - $this->setExpectedException( 'WPDieException', 'You do not have permission to access this content.' ); - - $this->main->reporting_page_init(); - - } - - /** - * Test reporting_page_init() when there's no permission issues - * - * @since 4.7.0 - * - * @return void - */ - public function test_reporting_page_init_permission_success() { - - set_current_screen( 'admin' ); - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockGetRequest( array( 'student_id' => $this->factory->student->create() ) ); - - $this->assertOutputContains( '<div class="wrap lifterlms llms-reporting tab--students">', array( $this->main, 'reporting_page_init' ) ); - - set_current_screen( 'front' ); - } - - /** - * Test reporting_page_init() when there's no permission issues - * - * @since 4.7.0 - * - * @return void - */ - public function test_reporting_page_init_no_permissions() { - - set_current_screen( 'admin' ); - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->assertOutputContains( '<div class="wrap lifterlms llms-reporting tab--students">', array( $this->main, 'reporting_page_init' ) ); - - set_current_screen( 'front' ); - } - - /** - * Test status_page_includes() - * - * @since 4.12.0 - * - * @return void - */ - public function test_status_page_includes() { - - $classes = array( - 'LLMS_Admin_Page_Status', - - 'LLMS_Admin_Tool_Batch_Eraser', - 'LLMS_Admin_Tool_Clear_Sessions', - 'LLMS_Admin_Tool_Recurring_Payment_Rescheduler', - ); - - $actions = did_action( 'llms_load_admin_tools' ); - - foreach ( $classes as $class ) { - $this->assertFalse( class_exists( $class ) ); - } - - LLMS_Unit_Test_Util::call_method( $this->main, 'status_page_includes' ); - - // Classes included. - foreach ( $classes as $class ) { - $this->assertTrue( class_exists( $class ) ); - } - - // Action ran. - $this->assertSame( ++$actions, did_action( 'llms_load_admin_tools' ) ); - - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-notices.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-notices.php deleted file mode 100644 index ff4fdf444f..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-notices.php +++ /dev/null @@ -1,495 +0,0 @@ -<?php -/** - * Test Admin Notices Class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group admin_notices - * - * @since 4.10.0 - */ -class LLMS_Test_Admin_Notices extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php'; - } - - /** - * Test add_output_actions(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_add_output_actions() { - - remove_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ); - - // Any screen. - LLMS_Admin_Notices::add_output_actions(); - $this->assertEquals( 10, has_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ) ); - - remove_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ); - - // LLMS settings screen. - set_current_screen( 'lifterlms_page_llms-settings' ); - - LLMS_Admin_Notices::add_output_actions(); - $this->assertEquals( 10, has_action( 'lifterlms_settings_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ) ); - - set_current_screen( 'front' ); - - } - - /** - * Test init() properly initializes the `$notices` class variable - * - * @since 4.10.0 - * - * @return void - */ - public function test_init_notices_var() { - - $expect = array( 'fake' ); - update_option( 'llms_admin_notices', $expect ); - - LLMS_Admin_Notices::init(); - - $this->assertEquals( $expect, LLMS_Admin_Notices::get_notices() ); - - } - - /** - * Test init() properly adds action hooks - * - * @since 4.10.0 - * - * @return void - */ - public function test_init_add_actions() { - - remove_action( 'wp_loaded', array( 'LLMS_Admin_Notices', 'hide_notices' ) ); - remove_action( 'current_screen', array( 'LLMS_Admin_Notices', 'add_output_actions' ) ); - remove_action( 'shutdown', array( 'LLMS_Admin_Notices', 'save_notices' ) ); - - LLMS_Admin_Notices::init(); - - $this->assertEquals( 10, has_action( 'wp_loaded', array( 'LLMS_Admin_Notices', 'hide_notices' ) ) ); - $this->assertEquals( 10, has_action( 'current_screen', array( 'LLMS_Admin_Notices', 'add_output_actions' ) ) ); - $this->assertEquals( 10, has_action( 'shutdown', array( 'LLMS_Admin_Notices', 'save_notices' ) ) ); - - } - - /** - * Test add_notice() for a notice that has been previously dismissed - * - * @since 4.13.0 - * - * @return void - */ - public function test_add_notice_already_dismissed() { - - set_transient( 'llms_admin_notice_test-dismissal_delay', 'yes', 60 ); - - LLMS_Admin_Notices::add_notice( 'test-dismissal' ); - - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'test-dismissal' ) ); - - } - - /** - * Test add_notice() with HTML and defaults - * - * @since 4.13.0 - * - * @return void - */ - public function test_add_notice_with_defaults() { - - LLMS_Admin_Notices::add_notice( 'test-add-notice', '<p>HTML CONTENT</p>' ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-add-notice' ) ); - - $this->assertEquals( array( - 'dismissible' => true, - 'dismiss_for_days' => 7, - 'flash' => false, - 'html' => '<p>HTML CONTENT</p>', - 'remind_in_days' => 7, - 'remindable' => false, - 'type' => 'info', - 'template' => false, - 'template_path' => '', - 'default_path' => '', - ), LLMS_Admin_Notices::get_notice( 'test-add-notice' ) ); - - } - - /** - * Test add_notice() with HTML and defaults - * - * @since 4.13.0 - * - * @return void - */ - public function test_add_notice_with_options() { - - LLMS_Admin_Notices::add_notice( 'test-add-notice-2', array( 'template' => 'path/to/template.php' ) ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-add-notice-2' ) ); - - $this->assertEquals( array( - 'dismissible' => true, - 'dismiss_for_days' => 7, - 'flash' => false, - 'html' => '', - 'remind_in_days' => 7, - 'remindable' => false, - 'type' => 'info', - 'template' => 'path/to/template.php', - 'template_path' => '', - 'default_path' => '', - ), LLMS_Admin_Notices::get_notice( 'test-add-notice-2' ) ); - - } - - /** - * Test delete_notice() - * - * @since 4.13.0 - * - * @return void - */ - public function test_delete_notice() { - - LLMS_Admin_Notices::add_notice( 'test-delete' ); - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete' ) ); - - LLMS_Admin_Notices::delete_notice( 'test-delete' ); - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete' ) ); - - $this->assertSame( 1, did_action( 'lifterlms_delete_test-delete_notice' ) ); - $this->assertFalse( get_transient( 'llms_admin_notice_test-delete_delay' ) ); - - } - - /** - * Test delete_notice() when "reminding" for a notice that is not remindable - * - * @since 4.13.0 - * - * @return void - */ - public function test_delete_notice_remind_not_remindable() { - - LLMS_Admin_Notices::add_notice( 'test-delete-not-remindable' ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete-not-remindable' ) ); - - LLMS_Admin_Notices::delete_notice( 'test-delete-not-remindable', 'remind' ); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete-not-remindable' ) ); - $this->assertFalse( get_transient( 'llms_admin_notice_test-delete-not-remindable_delay' ) ); - $this->assertSame( 1, did_action( 'lifterlms_remind_test-delete-not-remindable_notice' ) ); - - } - - /** - * Test delete_notice() for a remindable notice - * - * @since 4.13.0 - * - * @return void - */ - public function test_delete_notice_remind() { - - LLMS_Admin_Notices::add_notice( 'test-remind', array( 'remindable' => true ) ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-remind' ) ); - - LLMS_Admin_Notices::delete_notice( 'test-remind', 'remind' ); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-remind' ) ); - $this->assertEquals( 'yes', get_transient( 'llms_admin_notice_test-remind_delay' ) ); - $this->assertSame( 1, did_action( 'lifterlms_remind_test-remind_notice' ) ); - - - } - - /** - * Test delete_notice() for dismissing a not dismissible notice - * - * @since 4.13.0 - * - * @return void - */ - public function test_delete_notice_remind_not_dismissable() { - - LLMS_Admin_Notices::add_notice( 'test-delete-not-dismissible', array( 'dismissible' => false ) ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete-not-dismissible' ) ); - - LLMS_Admin_Notices::delete_notice( 'test-delete-not-dismissible', 'hide' ); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete-not-dismissible' ) ); - $this->assertFalse( get_transient( 'llms_admin_notice_test-delete-not-dismissible_delay' ) ); - $this->assertSame( 1, did_action( 'lifterlms_hide_test-delete-not-dismissible_notice' ) ); - - } - - /** - * Test delete_notice() for a dismissible notice - * - * @since 4.13.0 - * - * @return void - */ - public function test_delete_notice_dismiss() { - - LLMS_Admin_Notices::add_notice( 'test-dismiss' ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-dismiss' ) ); - - LLMS_Admin_Notices::delete_notice( 'test-dismiss', 'hide' ); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-dismiss' ) ); - $this->assertEquals( 'yes', get_transient( 'llms_admin_notice_test-dismiss_delay' ) ); - $this->assertSame( 1, did_action( 'lifterlms_hide_test-dismiss_notice' ) ); - - } - - /** - * Test flash_notice() - * - * @since 4.13.0 - * - * @return void - */ - public function test_flash_notice() { - - LLMS_Admin_Notices::flash_notice( '<p>FLASH NOTICE</p>', 'error' ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'llms-flash-notice-0' ) ); - $this->assertEquals( array( - 'dismissible' => false, - 'dismiss_for_days' => 7, - 'flash' => true, - 'html' => '<p>FLASH NOTICE</p>', - 'remind_in_days' => 7, - 'remindable' => false, - 'type' => 'error', - 'template' => '', - 'template_path' => '', - 'default_path' => '', - ), LLMS_Admin_Notices::get_notice( 'llms-flash-notice-0' ) ); - - // Test incrementor. - LLMS_Admin_Notices::flash_notice( '<p>FLASH NOTICE 2</p>', 'success' ); - - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'llms-flash-notice-1' ) ); - $this->assertEquals( array( - 'dismissible' => false, - 'dismiss_for_days' => 7, - 'flash' => true, - 'html' => '<p>FLASH NOTICE 2</p>', - 'remind_in_days' => 7, - 'remindable' => false, - 'type' => 'success', - 'template' => '', - 'template_path' => '', - 'default_path' => '', - ), LLMS_Admin_Notices::get_notice( 'llms-flash-notice-1' ) ); - - - } - - /** - * Test get_notice() - * - * @since 4.13.0 - * - * @return void - */ - public function test_get_notice() { - - LLMS_Admin_Notices::add_notice( 'test-get' ); - - $this->assertEquals( array( - 'dismissible' => true, - 'dismiss_for_days' => 7, - 'flash' => false, - 'html' => '', - 'remind_in_days' => 7, - 'remindable' => false, - 'type' => 'info', - 'template' => false, - 'template_path' => '', - 'default_path' => '', - ), LLMS_Admin_Notices::get_notice( 'test-get' ) ); - - } - - public function test_get_notice_not_found() { - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-get-not-found' ) ); - - } - - /** - * Test get_notices() - * - * @since 4.13.0 - * - * @return void - */ - public function test_get_notices() { - - // Reset the array from previous tests. - LLMS_Admin_Notices::init(); - - LLMS_Admin_Notices::add_notice( 'test-get-all' ); - LLMS_Admin_Notices::add_notice( 'test-get-all-2' ); - $this->assertEquals( array( 'test-get-all', 'test-get-all-2' ), LLMS_Admin_Notices::get_notices() ); - - } - - /** - * Test get_notices() when no notices record exists in the DB - * - * @since 4.13.0 - * - * @return void - */ - public function test_get_notices_no_db_option() { - - delete_option( 'llms_admin_notices' ); - - // Reset the array from previous tests. - LLMS_Admin_Notices::init(); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notices() ); - - } - - /** - * Test get_notices() when an empty string is stored in the DB option - * - * @since 4.13.0 - * - * @link https://github.com/gocodebox/lifterlms/issues/1443 - * - * @return void - */ - public function test_get_notices_empty_string_db_option() { - - update_option( 'llms_admin_notices', '' ); - - // Reset the array from previous tests. - LLMS_Admin_Notices::init(); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notices() ); - - } - - /** - * Test get_notices() when malformed or invalid data is stored in the DB. - * - * @since 4.13.0 - * - * @return void - */ - public function test_get_notices_invalid_db_option() { - - update_option( 'llms_admin_notices', array( array(), 1, null, new stdClass() ) ); - - // Reset the array from previous tests. - LLMS_Admin_Notices::init(); - - $this->assertEquals( array(), LLMS_Admin_Notices::get_notices() ); - - } - - /** - * Test has_notice() - * - * @since 4.13.0 - * - * @return void - */ - public function test_has_notice() { - - $id = 'test-has'; - $this->assertFalse( LLMS_Admin_Notices::has_notice( $id ) ); - - LLMS_Admin_Notices::add_notice( $id ); - $this->assertTrue( LLMS_Admin_Notices::has_notice( $id ) ); - - } - - /** - * Test output_notice(). - * - * @since 5.3.1 - * - * @return void - */ - public function test_output_notice() { - - LLMS_Admin_Notices::init(); - - # Create a normal notice. - $notice_html = 'Have you heard of the band 999 MB? They haven\'t got a gig yet.'; - $notice_id = 'test-output-notice-normal'; - LLMS_Admin_Notices::add_notice( $notice_id, $notice_html ); - LLMS_Admin_Notices::save_notices(); - - # Test where current user does not have the 'manage_options' capability. - $this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) ); - - # Test where current user does have the 'manage_options' capability. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertOutputContains( $notice_html, array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) ); - - # Test where the notice does not exist. - $this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( 'notice-does-not-exist' ) ); - - # Test where the notice html is empty. - $notice_id = 'test-output-notice-empty-html-empty-template'; - LLMS_Admin_Notices::add_notice( $notice_id, '' ); - LLMS_Admin_Notices::save_notices(); - $this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) ); - - } - - /** - * Test save_notices() - * - * @since 4.13.0 - * - * @return void - */ - public function test_save_notices() { - - // Reset the array from previous tests. - LLMS_Admin_Notices::init(); - - LLMS_Admin_Notices::add_notice( 'test-save-1' ); - LLMS_Admin_Notices::add_notice( 'test-save-2' ); - - LLMS_Admin_Notices::save_notices(); - - $this->assertEquals( array( 'test-save-1', 'test-save-2' ), get_option( 'llms_admin_notices' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-page-status.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-page-status.php deleted file mode 100644 index 94273c5a4e..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-page-status.php +++ /dev/null @@ -1,196 +0,0 @@ -<?php -/** - * Test Admin Status page - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group status - * - * @since 3.37.14 - * @since 4.0.0 Removed clear sessions tests in favor of tests in the `LLMS_Test_Admin_Tool_Clear_Sessions` test class. - */ -class LLMS_Test_Admin_Page_Status extends LLMS_Unit_Test_Case { - - /** - * Set up before class - * - * @since Unknown - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.page.status.php'; - - } - - /** - * Setup the test case - * - * @since 3.37.14 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = 'LLMS_Admin_Page_Status'; - - } - - /** - * Test do_tool() when no nonce is submitted. - * - * @since 3.37.14 - * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation. - * - * @return void - */ - public function test_do_tool_no_nonce() { - - $this->expectException( 'WPDieException' ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - } - - /** - * Test do_tool() when invalid nonce is submitted. - * - * @since 3.37.14 - * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation. - * - * @return void - */ - public function test_do_tool_invalid_nonce() { - - $this->expectException( 'WPDieException' ); - - $this->mockPostRequest( array( - '_wpnonce' => 'fake', - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - } - - /** - * Test do_tool() when no user permissions - * - * @since 3.37.14 - * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation. - * - * @return void - */ - public function test_do_tool_no_user_caps() { - - $this->expectException( 'WPDieException' ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'llms_tool' ), - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - } - - /** - * Test do_tool() valid. - * - * @since 3.37.14 - * - * @return void - */ - public function test_do_tool_valid_user() { - - $actions = did_action( 'llms_status_tool' ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'llms_tool' ), - 'llms_tool' => 'custom', - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - $this->assertEquals( ++$actions, did_action( 'llms_status_tool' ) ); - - } - - /** - * Test the overall progress cache clear tool. - * - * @since 3.37.14 - * - * @return void - */ - public function test_do_tool_clear_cache() { - - // Add mock data. - foreach ( $this->factory->student->create_many( 3 ) as $uid ) { - update_user_meta( $uid, 'llms_overall_progress', 'mock' ); - update_user_meta( $uid, 'llms_overall_grade', 'mock' ); - } - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'llms_tool' ), - 'llms_tool' => 'clear-cache', - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - global $wpdb; - $res = $wpdb->get_results( "SELECT * FROM {$wpdb->usermeta} WHERE meta_key = 'llms_overall_progress' OR meta_key = 'llms_overall_grade';" ); - - $this->assertEquals( array(), $res ); - - } - - /** - * Test the tracking reset tool. - * - * @since 3.37.14 - * - * @return void - */ - public function test_do_tool_reset_tracking() { - - update_option( 'llms_allow_tracking', 'yes' ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'llms_tool' ), - 'llms_tool' => 'reset-tracking', - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - $this->assertEquals( 'no', get_option( 'llms_allow_tracking' ) ); - - } - - /** - * Test the setup wizard redirect tool. - * - * @since 3.37.14 - * @since 4.13.0 Fix expected redirect URL. - * - * @return void - */ - public function test_do_tool_setup_wizard() { - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', admin_url( 'admin.php?page=llms-setup') ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'llms_tool' ), - 'llms_tool' => 'setup-wizard', - ) ); - LLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-profile.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-profile.php deleted file mode 100644 index f63439d381..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-profile.php +++ /dev/null @@ -1,157 +0,0 @@ -<?php -/** - * Test Admin Profile Class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group admin_profile - * - * @since 5.0.0 - */ -class LLMS_Test_Admin_Profile extends LLMS_Unit_Test_Case { - - /** - * Set Up Before Class - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-profile.php'; - - } - - /** - * Set-Up - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Admin_Profile(); - - } - - /** - * Tear down - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - wp_set_current_user( null ); - - } - - /** - * Test current_user_can_edit_admin_custom_fields() method - * - * @since 5.0.0 - * - * @return void - */ - public function test_current_user_can_edit_admin_custom_fields() { - - $func = LLMS_Unit_Test_Util::get_private_method( $this->main, 'current_user_can_edit_admin_custom_fields' ); - - // No user logged in. - $this->assertFalse( - $func->invokeArgs( $this->main, array( null ) ) // No user passed. - ); - - $user = $this->factory->user->create(); - - $this->assertFalse( - $func->invokeArgs( $this->main, array( $user ) ) - ); - - // Create a subscriber. - $subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) ); - // Log-in. - wp_set_current_user( $subscriber ); - - // Still cannot manage the other user custom fields. - $this->assertFalse( - $func->invokeArgs( $this->main, array( $user ) ) - ); - - // Create an admin. - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - // Log-in. - wp_set_current_user( $admin ); - - $this->assertTrue( - $func->invokeArgs( $this->main, array( $user ) ) - ); - - } - - /** - * Test add_user_meta_fields() - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_user_meta_fields() { - - $user = $this->factory->user->create(); - - // No logged-in user. - $this->assertFalse( - $this->main->add_user_meta_fields( $user ) - ); - - // Create an admin. - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - // Log-in. - wp_set_current_user( $admin ); - - // Admin user logged-in. - ob_start(); // ob_start/ob_end_clean wrapper to avoid the view printing (via `include_once`). - $this->assertTrue( - $this->main->add_user_meta_fields( $user ) - ); - $this->assertTrue( - $this->main->add_user_meta_fields( $admin ) - ); - ob_end_clean(); - - // Simple user logged-in: no required caps. - wp_set_current_user( $user ); - $this->assertFalse( - $this->main->add_user_meta_fields( $user ) - ); - $this->assertFalse( - $this->main->add_user_meta_fields( $admin ) - ); - - // Admin user logged-in but empty custom fields. - wp_set_current_user( $admin ); - LLMS_Unit_Test_Util::set_private_property( $this->main, 'fields', null ); - - add_filter( 'llms_admin_profile_fields', '__return_empty_array' ); - - $this->assertFalse( - $this->main->add_user_meta_fields( $user ) - ); - $this->assertFalse( - $this->main->add_user_meta_fields( $admin ) - ); - - remove_filter( 'llms_admin_profile_fields', '__return_empty_array' ); - - } -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-review.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-review.php deleted file mode 100644 index ed8af349f4..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-review.php +++ /dev/null @@ -1,310 +0,0 @@ -<?php -/** - * Tests for LLMS_Admin_Review class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group admin_reviews - * - * @since 3.24.0 - */ -class LLMS_Test_Admin_Review extends LLMS_UnitTestCase { - - /** - * Setup test class - * - * @since 4.14.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-review.php'; - - } - - /** - * Setup test case - * - * @since 3.24.0 - * @since 4.14.0 Move file include into `setUpBeforeClass()`. - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Admin_Review(); - - } - - /** - * Test admin_footer() when it's not supposed to display - * - * @since 4.14.0 - * - * @return void - */ - public function test_admin_footer_screen_not_set() { - $this->assertEquals( 'fake', $this->main->admin_footer( 'fake' ) ); - } - - /** - * Test admin_footer() when it's supposed to display - * - * @since 4.14.0 - * - * @return void - */ - public function test_admin_footer_screen_on_lifterlms_screen() { - - set_current_screen( 'lifterlms' ); - $this->assertEquals( 'Please rate <strong>LifterLMS</strong> <a href="https://wordpress.org/support/plugin/lifterlms/reviews/?filter=5#new-post" target="_blank" rel="noopener noreferrer">★★★★★</a> on <a href="https://wordpress.org/support/plugin/lifterlms/reviews/?filter=5#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' ) ); - set_current_screen( 'front' ); - - } - - /** - * Test dismiss() for a logged out user with no nonce - * - * @since 4.14.0 - * - * @return void - */ - public function test_dismiss_permissions_logged_out_no_nonce() { - - try { - $this->main->dismiss(); - } catch ( WPDieException $e ) { - $this->assertSame( '', get_option( 'llms_review', '' ) ); - } - - } - - /** - * Test dismiss() for a valid user with no nonce - * - * @since 4.14.0 - * - * @return void - */ - public function test_dismiss_permissions_logged_in_invalid_nonce() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'success' => 'yes', - 'nonce' => 'fake', - ) ); - - try { - $this->main->dismiss(); - } catch ( WPDieException $e ) { - $this->assertSame( '', get_option( 'llms_review', '' ) ); - } - - } - - /** - * Test dismiss() when the user goes to wp.org - * - * @since 4.14.0 - * - * @return void - */ - public function test_dismiss_success() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'success' => 'yes', - 'nonce' => wp_create_nonce( 'llms-admin-review-request-dismiss' ), - ) ); - - try { - $this->main->dismiss(); - } catch ( WPDieException $e ) { - - $this->assertEquals( array( - 'time' => time(), - 'dismissed' => true, - 'success' => 'yes', - ), get_option( 'llms_review' ) ); - - } - - } - - - /** - * Test dismiss() when the user ignores/dismissed - * - * @since 4.14.0 - * - * @return void - */ - public function test_dismiss_nope() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - 'success' => 'no', - 'nonce' => wp_create_nonce( 'llms-admin-review-request-dismiss' ), - ) ); - - try { - $this->main->dismiss(); - } catch ( WPDieException $e ) { - - $this->assertEquals( array( - 'time' => time(), - 'dismissed' => true, - 'success' => 'no', - ), get_option( 'llms_review' ) ); - - } - - } - - /** - * Test maybe_show_notice() when logged out. - * - * @since 4.14.0 - * - * @return void - */ - public function test_maybe_show_notice_no_user() { - $this->assertNull( $this->main->maybe_show_notice() ); - } - - /** - * Test maybe_show_notice() on its first run - * - * @since 4.14.0 - * - * @return void - */ - public function test_maybe_show_notice_first_run() { - - delete_option( 'llms_review' ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertFalse( $this->main->maybe_show_notice() ); - - $this->assertEquals( array( - 'time' => time(), - 'dismissed' => false, - ), get_option( 'llms_review' ) ); - - } - - /** - * Test maybe_show_notice() - * - * @since 4.14.0 - * - * @return void - */ - public function test_maybe_show() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->factory->student->create_and_enroll_many( 30, $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - - // Already Dismissed. - update_option( 'llms_review', array( - 'time' => time() - YEAR_IN_SECONDS, - 'dismissed' => true, - ) ); - $this->assertFalse( $this->main->maybe_show_notice() ); - - // Too soon. - update_option( 'llms_review', array( - 'time' => time() - HOUR_IN_SECONDS, - 'dismissed' => false, - ) ); - $this->assertFalse( $this->main->maybe_show_notice() ); - - // Okay. - update_option( 'llms_review', array( - 'time' => time() - YEAR_IN_SECONDS, - 'dismissed' => false, - ) ); - - $output = $this->get_output( array( $this->main, 'maybe_show_notice' ) ); - - $this->assertStringContains( '<div class="notice notice-info is-dismissible llms-review-notice">', $output ); - - } - - /** - * Test maybe_show_notice() when the notice would display (assuming there were enough enrollments) - * - * @since 4.14.0 - * - * @return void - */ - public function test_maybe_show_too_few_enrollments() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Okay. - update_option( 'llms_review', array( - 'time' => time() - YEAR_IN_SECONDS, - 'dismissed' => false, - ) ); - - $this->assertFalse( $this->main->maybe_show_notice() ); - - } - - /** - * Test round_down(). - * - * @since 3.24.0 - * @since 4.14.0 Use a loop. - * - * @return void - */ - public function test_round_down() { - - $tests = array( - // Expected, Input. - array( 1, 1 ), - array( 5, 5 ), - array( 9, 9 ), - array( 10, 11 ), - array( 20, 25 ), - array( 30, 37 ), - array( 40, 40 ), - array( 50, 58 ), - array( 60, 63 ), - array( 70, 72 ), - array( 80, 88 ), - array( 90, 99 ), - array( 100, 105 ), - array( 200, 293 ), - array( 300, 392 ), - array( 500, 532 ), - array( 700, 781 ), - array( 800, 850 ), - array( 900, 900 ), - array( 1000, 1000 ), - array( 1000, 1101 ), - array( 1000, 1500 ), - array( 2000, 2205 ), - array( 5000, 5878 ), - array( 9000, 9999 ), - array( 10000, 10000 ), - array( 10000, 10001 ), - array( 10000, 10299 ), - array( 10000, 50099 ), - ); - - foreach ( $tests as $vals ) { - $this->assertEquals( $vals[0], LLMS_Admin_Review::round_down( $vals[1] ) ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-setup-wizard.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-setup-wizard.php deleted file mode 100644 index b41bfbc269..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-setup-wizard.php +++ /dev/null @@ -1,608 +0,0 @@ -<?php -/** - * Test Setup Wizard - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group setup_wizard - * - * @since 4.8.0 - */ -class LLMS_Test_Admin_Setup_Wizard extends LLMS_Unit_Test_Case { - - /** - * Setup Before Class - * - * Include required class files - * - * @since 4.8.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.setup.wizard.php'; - - } - - /** - * Setup test case - * - * @since 4.8.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Admin_Setup_Wizard(); - - } - - /** - * Test constructor - * - * @since 4.8.0 - * - * @return void - */ - public function test_constructor() { - - foreach ( array( '__return_true' => 10, '__return_false' => false ) as $func => $expect ) { - - add_filter( 'llms_enable_setup_wizard', $func ); - - remove_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) ); - remove_action( 'admin_menu', array( $this->main, 'admin_menu' ) ); - remove_action( 'admin_init', array( $this->main, 'save' ) ); - - $this->assertEquals( false, has_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) ) ); - $this->assertEquals( false, has_action( 'admin_menu', array( $this->main, 'admin_menu' ) ) ); - $this->assertEquals( false, has_action( 'admin_init', array( $this->main, 'save' ) ) ); - - $this->main = new LLMS_Admin_Setup_Wizard(); - - $this->assertEquals( $expect, has_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) ) ); - $this->assertEquals( $expect, has_action( 'admin_menu', array( $this->main, 'admin_menu' ) ) ); - $this->assertEquals( $expect, has_action( 'admin_init', array( $this->main, 'save' ) ) ); - - remove_filter( 'llms_enable_setup_wizard', $func ); - - } - - } - - /** - * Test admin_menu() - * - * @since 4.8.0 - * - * @return void - */ - public function test_admin_menu() { - - // No user. - $this->assertFalse( $this->main->admin_menu() ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertEquals( 'admin_page_llms-setup', $this->main->admin_menu() ); - - $this->assertEquals( 'yes', get_option( 'lifterlms_first_time_setup' ) ); - - // Clean up. - delete_option( 'lifterlms_first_time_setup' ); - wp_set_current_user( null ); - - } - - /** - * Test enqueue() - * - * @since 4.8.0 - * - * @return void - */ - public function test_enqueue() { - - $this->assertTrue( $this->main->enqueue() ); - - } - - /** - * Test get_completed_url(). - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_completed_url() { - - $ids = $this->factory->course->create_many( 3, array( 'sections' => 0 ) ); - - // More than one course redirects to the course post table. - $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 ) ) ); - unset( $ids[2] ); - $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 ) ) ); - - // One course goes to the the course's edit page. - unset( $ids[1] ); - $this->assertEquals( get_edit_post_link( $ids[0], 'not-display' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_completed_url', array( $ids ) ) ); - - } - - /** - * Test get_current_step() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_current_step() { - - $this->assertEquals( 'intro', $this->main->get_current_step() ); - - $this->mockGetRequest( array( 'step' => 'mock' ) ); - $this->assertEquals( 'mock', $this->main->get_current_step() ); - - } - - /** - * Test get_next_step() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_next_step() { - - // Not found. - $this->assertFalse( $this->main->get_next_step( 'fake' ) ); - - // No next step. - $this->assertFalse( $this->main->get_next_step( 'finish' ) ); - - $this->assertEquals( 'pages', $this->main->get_next_step( 'intro' ) ); - - $this->mockGetRequest( array( 'step' => 'intro' ) ); - $this->assertEquals( 'pages', $this->main->get_next_step() ); - - } - - - /** - * Test get_prev_step() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_prev_step() { - - // Not found. - $this->assertFalse( $this->main->get_prev_step( 'fake' ) ); - - // No previous step. - $this->assertFalse( $this->main->get_prev_step( 'intro' ) ); - - $this->assertEquals( 'coupon', $this->main->get_prev_step( 'finish' ) ); - - $this->mockGetRequest( array( 'step' => 'finish' ) ); - $this->assertEquals( 'coupon', $this->main->get_prev_step() ); - - } - - /** - * Test get_save_text() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_save_text() { - - $this->assertEquals( 'Allow', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'coupon' ) ) ); - $this->assertEquals( 'Import Courses', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'finish' ) ) ); - - $this->assertEquals( 'Save & Continue', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'anything-else' ) )); - - } - - /** - * Test get_save_text() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_skip_text() { - - $this->assertEquals( 'No thanks', LLMS_Unit_Test_Util::call_method( $this->main, 'get_skip_text', array( 'coupon' ) ) ); - $this->assertEquals( 'Skip this step', LLMS_Unit_Test_Util::call_method( $this->main, 'get_skip_text', array( 'anything-else' ) )); - - } - - /** - * Test get_step_url() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_step_url() { - - $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' ) ) ); - - } - - /** - * Test get_steps() - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_steps() { - - $res = $this->main->get_steps(); - $this->assertTrue( is_array( $res ) ); - foreach ( $res as $key => $val ) { - $this->assertTrue( ! empty( $key ) ); - $this->assertTrue( ! empty( $val ) ); - $this->assertTrue( is_string( $key ) ); - $this->assertTrue( is_string( $val ) ); - } - - } - - /** - * Test output() - * - * @since 4.8.0 - * - * @return void - */ - public function test_output() { - - $output = $this->get_output( array( $this->main, 'output' ), array( 'intro' ) ); - - $this->assertStringContains( '<div id="llms-setup-wizard">', $output ); - $this->assertStringContains( '<h1 id="llms-logo">', $output ); - $this->assertStringContains( '<ul class="llms-setup-progress">', $output ); - - } - - /** - * Test save() when there are nonce or user permission issues - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_permissions_issues() { - - // No nonce. - $this->assertNull( $this->main->save() ); - - // Invalid nonce. - $data = array( - 'llms_setup_nonce' => 'fake', - ); - $this->mockPostRequest( $data ); - $this->assertNull( $this->main->save() ); - - // Missing user. - $data = array( - 'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ), - ); - $this->mockPostRequest( $data ); - $this->assertNull( $this->main->save() ); - - } - - /** - * Test save() for an invalid step - * - * This test also covers an error response from any valid step. - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_invalid_step() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockPostRequest( array( - 'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ), - 'llms_setup_save' => 'fake-step', - ) ); - - $res = $this->main->save(); - - $this->assertIsWpError( $res ); - $this->assertWPErrorCodeEquals( 'llms-setup-save-invalid', $res ); - - $this->assertEquals( $res, $this->main->error ); - - } - - /** - * Test save() for success (and redirection) - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_success() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $this->mockGetRequest( array( - 'step' => 'pages', - ) ); - - $this->mockPostRequest( array( - 'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ), - 'llms_setup_save' => 'pages', - ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( 'http://example.org/wp-admin/?page=llms-setup&step=payments [302] YES' ); - - $this->main->save(); - - } - - /** - * Test save_coupon() when an http error is encountered - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_coupon_http_error() { - - $handler = function( $preempt, $args, $url ) { - if ( 'https://lifterlms.com/llms-api/tracking' === $url ) { - return new WP_Error( 'mock-err', 'Error' ); - } - return $preempt; - }; - - add_filter( 'pre_http_request', $handler, 10, 3 ); - - $ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' ); - - $this->assertIsWpError( $ret ); - $this->assertWPErrorCodeEquals( 'mock-err', $ret ); - - remove_filter( 'pre_http_request', $handler, 10 ); - - } - - /** - * Test save_coupon() when the tracking data api returns an error - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_coupon_api_error() { - - $handler = function( $preempt, $args, $url ) { - if ( 'https://lifterlms.com/llms-api/tracking' === $url ) { - return array( 'body' => json_encode( array( 'success' => false, 'message' => 'Server error' ) ) ); - } - return $preempt; - }; - - add_filter( 'pre_http_request', $handler, 10, 3 ); - - $ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' ); - - $this->assertIsWpError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-setup-coupon-save-tracking-api', $ret ); - - remove_filter( 'pre_http_request', $handler, 10 ); - - } - - /** - * Test save_coupon() when the tracking data api returns data in an unexpected format - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_coupon_unknown_error() { - - $handler = function( $preempt, $args, $url ) { - if ( 'https://lifterlms.com/llms-api/tracking' === $url ) { - return array( 'body' => json_encode( array() ) ); - } - return $preempt; - }; - - add_filter( 'pre_http_request', $handler, 10, 3 ); - - $ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' ); - - $this->assertIsWpError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-setup-coupon-save-unknown', $ret ); - - remove_filter( 'pre_http_request', $handler, 10 ); - - } - - /** - * Test save_coupon() success - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_coupon_success() { - - delete_option( 'llms_allow_tracking' ); - $handler = function( $preempt, $args, $url ) { - if ( 'https://lifterlms.com/llms-api/tracking' === $url ) { - return array( 'body' => json_encode( array( 'success' => true, 'message' => '' ) ) ); - } - return $preempt; - }; - - add_filter( 'pre_http_request', $handler, 10, 3 ); - - $ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' ); - - $this->assertTrue( $ret ); - $this->assertEquals( 'yes', get_option( 'llms_allow_tracking' ) ); - - remove_filter( 'pre_http_request', $handler, 10 ); - - } - - /** - * Test save_finish() when no import ids are provided - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_finish_error_no_ids() { - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' ) ); - - } - - /** - * Test save_finish() when an export api error occurs - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_finish_error_api() { - - $this->mockPostRequest( array( - 'llms_setup_course_import_ids' => array( 1 ), - ) ); - - $handler = function( $res ) { - return new WP_Error( 'mock', 'Mocked API response.' ); - }; - add_filter( 'pre_http_request', $handler ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' ); - $this->assertIsWpError( $res ); - $this->assertWPErrorCodeEquals( 'mock', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test save_finish() when an error is encountered during generation - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_finish_error_generator() { - - $this->mockPostRequest( array( - 'llms_setup_course_import_ids' => array( 1 ), - ) ); - - $handler = function( $res ) { - return array(); - }; - add_filter( 'pre_http_request', $handler ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' ); - $this->assertIsWpError( $res ); - $this->assertWPErrorCodeEquals( 'missing-generator', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test save_finish() for success - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_finish_success() { - - $this->mockPostRequest( array( - 'llms_setup_course_import_ids' => array( 33579 ), // Free course template. - ) ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' ); - - foreach ( $res as $id ) { - - $this->assertTrue( is_numeric( $id ) ); - $this->assertEquals( 'course', get_post_type( $id ) ); - - } - - } - - /** - * Test save_pages() - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_pages() { - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_pages' ) ); - - } - - /** - * Test save_payments() - * - * @since 4.8.0 - * - * @return void - */ - public function test_save_payments() { - - // With values submitted. - $this->mockPostRequest( array( - 'country' => 'MOCK', - 'currency' => 'CURR', - 'manual_payments' => 'yes' - ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_payments' ) ); - - $this->assertEquals( 'MOCK', get_option( 'lifterlms_country' ) ); - $this->assertEquals( 'CURR', get_option( 'lifterlms_currency' ) ); - $this->assertEquals( 'yes', get_option( 'llms_gateway_manual_enabled' ) ); - - delete_option( 'lifterlms_country' ); - delete_option( 'lifterlms_currency' ); - delete_option( 'llms_gateway_manual_enabled' ); - - // No values, use the defaults. - $this->mockPostRequest( array() ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_payments' ) ); - - $this->assertEquals( 'US', get_option( 'lifterlms_country' ) ); - $this->assertEquals( 'USD', get_option( 'lifterlms_currency' ) ); - $this->assertEquals( 'no', get_option( 'llms_gateway_manual_enabled' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-admin-users-table.php b/tests/phpunit/unit-tests/admin/class-llms-test-admin-users-table.php deleted file mode 100644 index 23f536ecbc..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-admin-users-table.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php -/** - * Test LLMS_Admin_Users_Table class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group users_table - * - * @since 4.0.0 - */ -class LLMS_Test_Admin_Users_table extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 4.0.0 - * @since 4.7.0 Add `LLMS_Admin_Reporting` class. - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/class.llms.admin.reporting.php'; - require_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-users-table.php'; - } - - /** - * Setup the test case - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - set_current_screen( 'users.php' ); - $this->main = new LLMS_Admin_Users_Table(); - - } - - - /** - * Teardown the test case - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - /** - * Reset current screen - * - * I can't find anything officially documenting the proper way to do this but this line seems to indicate - * you can reset it by using `front` as the current screen: - * - * https://core.trac.wordpress.org/browser/tags/5.4/src/wp-admin/includes/class-wp-screen.php#L277 - * - * Without this, tests following theses tests these tests which use function that have `is_admin()` calls in them - * may fail because `is_admin()` would otherwise return `true` on PHP 7.3 and lower and WP 5.2 or lower. - */ - set_current_screen( 'front' ); - - } - - /** - * Test add_actions() method - * - * @since 4.0.0 - * - * @return void - */ - public function test_add_actions() { - - $user = $this->factory->user->create_and_get(); - $res = $this->main->add_actions( array(), $user ); - - $this->assertArrayHasKey( 'llms-reporting', $res ); - - $this->assertStringContains( 'page=llms-reporting', $res['llms-reporting'] ); - $this->assertStringContains( 'tab=students', $res['llms-reporting'] ); - $this->assertStringContains( 'student_id=' . $user->ID, $res['llms-reporting'] ); - - } - - /** - * Test add_cols() - * - * @since 4.0.0 - * - * @return void - */ - public function test_add_cols() { - - $this->assertEquals( array( - 'llms-last-login' => 'Last Login', - 'llms-enrollments' => 'Enrollments', - ), $this->main->add_cols( array() ) ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-export-api.php b/tests/phpunit/unit-tests/admin/class-llms-test-export-api.php deleted file mode 100644 index 895d654c63..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-export-api.php +++ /dev/null @@ -1,125 +0,0 @@ -<?php -/** - * Test export api class - * - * @package LifterLMS/Tests/Admin - * - * @group admin - * @group export_api - * - * @since 4.8.0 - */ -class LLMS_Test_Export_API extends LLMS_Unit_Test_Case { - - /** - * Setup before class. - * - * @since 4.8.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php'; - } - - /** - * Test get() when a request error is encountered. - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_conn_error() { - - $handler = function( $res ) { - return new WP_Error( 'mocked', 'Mocked error' ); - }; - - add_filter( 'pre_http_request', $handler ); - - $res = LLMS_Export_API::get( array( 1 ) ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'mocked', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test get() when an API error is encountered (404) - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_api_error() { - - $res = LLMS_Export_API::get( array( 1 ) ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'not-found', $res ); - - } - - /** - * Test get() for success response - * - * @since 4.8.0 - * - * @return void - */ - public function test_get_success() { - - $res = LLMS_Export_API::get( array( 33579 ) ); // Free course lead magnet template. - - $this->assertEquals( 'LifterLMS/BulkCourseExporter', $res['_generator'] ); - $this->assertArrayHasKey( 33579, $res['courses'] ); - - } - - /** - * Test list() when a request error is encountered. - * - * @since 4.8.0 - * - * @return void - */ - public function test_list_conn_error() { - - $handler = function( $res ) { - return new WP_Error( 'mocked', 'Mocked error' ); - }; - - add_filter( 'pre_http_request', $handler ); - - $res = LLMS_Export_API::list(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'mocked', $res ); - - remove_filter( 'pre_http_request', $handler ); - - } - - /** - * Test list() for success response - * - * @since 4.8.0 - * - * @return void - */ - public function test_list_success() { - - $list = LLMS_Export_API::list(); - - $this->assertTrue( is_array( $list ) ); - - foreach ( $list as $res ) { - $this->assertEquals( array( 'id', 'description', 'image', 'title' ), array_keys( $res ) ); - } - - - } - - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-mailhawk.php b/tests/phpunit/unit-tests/admin/class-llms-test-mailhawk.php deleted file mode 100644 index 286488e9c1..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-mailhawk.php +++ /dev/null @@ -1,231 +0,0 @@ -<?php -/** - * Test MailHawk Connector - * - * @package LifterLMS/Tests - * - * @group mailhawk - * - * @since 3.40.0 - */ -class LLMS_Test_MailHawk extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 3.40.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - include_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-email-provider.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-mailhawk.php'; - - } - - /** - * Setup the test case. - * - * @since 3.40.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->mailhawk = new LLMS_MailHawk(); - - } - - /** - * Tear down the testcase. - * - * @since 3.40.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - wp_delete_file( WP_PLUGIN_DIR . '/mailhawk/uninstall.php' ); - delete_plugins( array( 'mailhawk/mailhawk.php' ) ); - - } - - /** - * Test the add_settings() method. - * - * @since 3.40.0 - * - * @return void - */ - public function test_add_settings() { - - // No settings for anyone without the `install_plugins` cap. - $this->assertEquals( array(), $this->mailhawk->add_settings( array() ) ); - - // Admin can see the settings. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $res = $this->mailhawk->add_settings( array() ); - $this->assertEquals( array( 'mailhawk_title', 'mailhawk_connect' ), wp_list_pluck( $res, 'id' ) ); - - } - - /** - * Test do_remote_install() error with no nonce submitted. - * - * @since 3.40.0 - * - * @return void - */ - public function test_do_remote_install_no_nonce() { - - $res = LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'do_remote_install' ); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'llms_mailhawk_install_nonce_failure', $res['code'] ); - $this->assertEquals( 401, $res['status'] ); - - } - - /** - * Test do_remote_install() error for no user. - * - * @since 3.40.0 - * - * @return void - */ - public function test_do_remote_install_no_user() { - - $this->mockPostRequest( array( - '_llms_mailhawk_nonce' => wp_create_nonce( 'llms-mailhawk-install' ), - ) ); - - $res = LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'do_remote_install' ); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'llms_mailhawk_install_unauthorized', $res['code'] ); - $this->assertEquals( 403, $res['status'] ); - - } - - /** - * Test do_remote_install() error with plugins api. - * - * @since 3.40.0 - * - * @return void - */ - public function test_do_remote_install_plugins_api_error() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - '_llms_mailhawk_nonce' => wp_create_nonce( 'llms-mailhawk-install' ), - ) ); - - $handler = function( $ret, $action, $args ) { - return new WP_Error( 'plugins_api_failed', 'Error' ); - }; - add_filter( 'plugins_api', $handler, 10, 3 ); - $res = LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'do_remote_install' ); - remove_filter( 'plugins_api', $handler, 10 ); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'plugins_api_failed', $res['code'] ); - $this->assertEquals( 400, $res['status'] ); - - } - - /** - * Test do remote install success. - * - * @since 3.40.0 - * - * @return void - */ - public function test_do_remote_install_success() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - '_llms_mailhawk_nonce' => wp_create_nonce( 'llms-mailhawk-install' ), - ) ); - - // Install. - $res = LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'do_remote_install' ); - $this->assertEquals( array( 'partner_id', 'register_url', 'client_state', 'redirect_uri', ), array_keys( $res ) ); - $this->assertEquals( 3, $res['partner_id'] ); - - // Already installed, activate. - $res = LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'do_remote_install' ); - $this->assertEquals( array( 'partner_id', 'register_url', 'client_state', 'redirect_uri', ), array_keys( $res ) ); - $this->assertEquals( 3, $res['partner_id'] ); - - } - - /** - * Test get_connect_setting() - * - * @since 3.40.0 - * - * @return void - */ - public function test_get_connect_setting() { - - // Not connected. - $this->assertStringContains( 'id="llms-mailhawk-connect"', LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'get_connect_setting' ) ); - - // Connected and not suspended. - update_option( 'mailhawk_is_connected', 'yes' ); - set_transient( 'mailhawk_is_suspended', 'no', 10 ); - $this->assertStringContains( 'View settings', LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'get_connect_setting' ) ); - $this->assertStringContains( 'manage your account', LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'get_connect_setting' ) ); - - // Connected and suspended. - set_transient( 'mailhawk_is_suspended', 'yes', 10 ); - $this->assertStringContains( 'Email sending is currently disabled', LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'get_connect_setting' ) ); - - } - - /** - * Test should_output_inline() method. - * - * @since 3.40.0 - * - * @return void - */ - public function test_should_output_inline() { - - // No user. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'should_output_inline' ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Wrong screen. - set_current_screen( 'admin' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'should_output_inline' ) ); - - // Mock screen. - set_current_screen( 'lifterlms_page_llms-settings' ); - - // Right screen, wrong tab. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'should_output_inline' ) ); - - // Right screen, right tab, is connected. - update_option( 'mailhawk_is_connected', 'yes' ); - $this->mockGetRequest( array( 'tab' => 'engagements' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'should_output_inline' ) ); - - // Right screen, right tab, not connected. - update_option( 'mailhawk_is_connected', 'no' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->mailhawk, 'should_output_inline' ) ); - - set_current_screen( 'front' ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/class-llms-test-sendwp.php b/tests/phpunit/unit-tests/admin/class-llms-test-sendwp.php deleted file mode 100644 index 1011cab65f..0000000000 --- a/tests/phpunit/unit-tests/admin/class-llms-test-sendwp.php +++ /dev/null @@ -1,234 +0,0 @@ -<?php -/** - * Test SendWP Connector - * - * @package LifterLMS/Tests - * - * @group sendwp - * - * @since 3.36.1 - * @since 3.37.0 Add testing for nonce verifications. - * @since 3.40.0 Added additional coverage. - */ -class LLMS_Test_SendWP extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 3.40.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - - include_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-email-provider.php'; - include_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-sendwp.php'; - - } - - /** - * Setup the test case. - * - * @since 3.36.1 - * @since 3.40.0 Include class file via `set_up_before_class()`. - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->sendwp = new LLMS_SendWP(); - - } - - /** - * Tear down the testcase. - * - * @since 3.36.1 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - delete_plugins( array( 'sendwp/sendwp.php' ) ); - - } - - /** - * Test the add_settings() method. - * - * @since 3.40.0 - * - * @return void - */ - public function test_add_settings() { - - // No settings for anyone without the `install_plugins` cap. - $this->assertEquals( array(), $this->sendwp->add_settings( array() ) ); - - // Admin can see the settings. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $res = $this->sendwp->add_settings( array() ); - $this->assertEquals( array( 'sendwp_title', 'sendwp_connect' ), wp_list_pluck( $res, 'id' ) ); - - } - - /** - * Test do_remote_install() error with no nonce submitted. - * - * @since 3.37.0 - * - * @return void - */ - public function test_do_remote_install_no_nonce() { - - $res = $this->sendwp->do_remote_install(); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'llms_sendwp_install_nonce_failure', $res['code'] ); - $this->assertEquals( 401, $res['status'] ); - - } - - /** - * Test do_remote_install() error for no user. - * - * @since 3.36.1 - * @since 3.37.0 Add mock nonce to test. - * - * @return void - */ - public function test_do_remote_install_no_user() { - - $this->mockPostRequest( array( - '_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ), - ) ); - - $res = $this->sendwp->do_remote_install(); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'llms_sendwp_install_unauthorized', $res['code'] ); - $this->assertEquals( 403, $res['status'] ); - - } - - /** - * Test do_remote_install() error with plugins api. - * - * @since 3.36.1 - * @since 3.37.0 Add mock nonce to test. - * - * @return void - */ - public function test_do_remote_install_plugins_api_error() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - '_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ), - ) ); - - $handler = function( $ret, $action, $args ) { - return new WP_Error( 'plugins_api_failed', 'Error' ); - }; - add_filter( 'plugins_api', $handler, 10, 3 ); - $res = $this->sendwp->do_remote_install(); - remove_filter( 'plugins_api', $handler, 10 ); - - $this->assertArrayHasKey( 'message', $res ); - $this->assertEquals( 'plugins_api_failed', $res['code'] ); - $this->assertEquals( 400, $res['status'] ); - - } - - /** - * Test do remote install success. - * - * @since 3.36.1 - * @since 3.37.0 Add mock nonce to test. - * - * @return void - */ - public function test_do_remote_install_success() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->mockPostRequest( array( - '_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ), - ) ); - - // Install. - $res = $this->sendwp->do_remote_install(); - $this->assertEquals( array( 'partner_id', 'register_url', 'client_name', 'client_secret', 'client_redirect', ), array_keys( $res ) ); - $this->assertEquals( 2007, $res['partner_id'] ); - - // Already installed, activate. - $res = $this->sendwp->do_remote_install(); - $this->assertEquals( array( 'partner_id', 'register_url', 'client_name', 'client_secret', 'client_redirect', ), array_keys( $res ) ); - $this->assertEquals( 2007, $res['partner_id'] ); - - } - - /** - * Test get_connect_setting() - * - * @since 3.40.0 - * - * @return void - */ - public function test_get_connect_setting() { - - // Not connected. - $this->assertStringContains( 'id="llms-sendwp-connect"', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) ); - - // Connected and forwarding. - update_option( 'sendwp_client_connected', '1' ); - $this->assertStringContains( 'Manage your account', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) ); - - // Connected and not forwarding. - update_option( 'sendwp_forwarding_enabled', '0' ); - $this->assertStringContains( 'Email sending is currently disabled', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) ); - - } - - /** - * Test should_output_inline() method. - * - * @since 3.40.0 - * - * @return void - */ - public function test_should_output_inline() { - - // No user. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Wrong screen. - set_current_screen( 'admin' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) ); - - // Mock screen. - set_current_screen( 'lifterlms_page_llms-settings' ); - - // Right screen, wrong tab. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) ); - - // Right screen, right tab, is connected. - update_option( 'sendwp_client_connected', '1' ); - $this->mockGetRequest( array( 'tab' => 'engagements' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) ); - - // Right screen, right tab, not connected. - update_option( 'sendwp_client_connected', '0' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) ); - - set_current_screen( 'front' ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-access.php b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-access.php deleted file mode 100644 index 591ac773ea..0000000000 --- a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-access.php +++ /dev/null @@ -1,136 +0,0 @@ -<?php -/** - * Tests for LifterLMS Order Metabox - * - * @package LifterLMS/Tests - * - * @group metabox_access - * @group admin - * @group metaboxes - * - * @since 3.36.1 - * @version 3.36.1 - */ -class LLMS_Test_Meta_Box_Access extends LLMS_PostTypeMetaboxTestCase { - - /** - * Setup test - * - * @since 3.36.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->metabox = new LLMS_Meta_Box_Access(); - - } - - /** - * Test the get_screens() method. - * - * @since 3.36.1 - * - * @return void - */ - public function test_get_screens() { - - $this->assertEquals( array( 'post', 'page' ), $this->metabox->get_screens() ); - - } - - /** - * Save with no user should fail. - * - * @since 3.36.1 - * - * @return [type] - */ - public function test_save_no_user() { - - $post = $this->factory->post->create(); - - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - - } - - /** - * Save with no nonce should fail. - * - * @since 3.36.1 - * - * @return [type] - */ - public function test_save_no_nonce() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $post = $this->factory->post->create(); - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - - } - - /** - * Save with invalid nonce will fail. - * - * @since 3.36.1 - * - * @return [type] - */ - public function test_save_invalid_nonce() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $post = $this->factory->post->create(); - $this->mockPostRequest( $this->add_nonce_to_array( array(), false ) ); - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - - } - - /** - * Test save method. - * - * @since 3.36.1 - * - * @return void - */ - public function test_save() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $post = $this->factory->post->create(); - $post_data = $this->add_nonce_to_array( array() ); - - // Nothing saved, value is reset. - $this->mockPostRequest( $post_data ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - $this->assertEquals( '', get_post_meta( $post, '_llms_is_restricted', true ) ); - - // Toggle restrictions on. - $post_data['_llms_is_restricted'] = 'yes'; - $this->mockPostRequest( $post_data ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - $this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) ); - - // Restrict to a single membership. - $post_data['_llms_restricted_levels'] = array( 1 ); - $this->mockPostRequest( $post_data ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - $this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) ); - $this->assertEquals( array( 1 ), get_post_meta( $post, '_llms_restricted_levels', true ) ); - - // Multiple memberships. - $post_data['_llms_restricted_levels'] = array( 2, 3 ); - $this->mockPostRequest( $post_data ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - $this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) ); - $this->assertEquals( array( 2, 3 ), get_post_meta( $post, '_llms_restricted_levels', true ) ); - - // Disable restrictions. - unset( $post_data['_llms_is_restricted'] ); - $this->mockPostRequest( $post_data ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) ); - $this->assertEquals( '', get_post_meta( $post, '_llms_is_restricted', true ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php deleted file mode 100644 index a58b78e3bb..0000000000 --- a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * Tests for LifterLMS Order Metabox - * - * @package LifterLMS/Tests - * - * @group metabox_lesson - * @group admin - * @group metaboxes - * - * @since 3.36.2 - * @version 3.36.2 - */ -class LLMS_Test_Meta_Box_Lesson extends LLMS_PostTypeMetaboxTestCase { - - /** - * Setup test - * - * @since 3.36.2 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->metabox = new LLMS_Meta_Box_Lesson(); - - } - - /** - * Test get fields. - * - * @since 3.36.2 - * - * @return void - */ - public function test_get_fields() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson = llms_get_post( $course->get_lessons( 'ids' )[0] ); - $post = $lesson->get( 'post' ); - $this->metabox->post = $post; - - // check the lessons Drip Settings methods list does not cointain 'start', - // as the course has no start date set. - foreach ( $this->metabox->get_fields() as $index => $f ) { - if ( 'Drip Settings' === $f['title'] ) { - $this->assertFalse( array_key_exists( 'start', $f['fields'][0]['value'] ) ); - break; - } - } - - // set a course start date./* - $course->set( 'start_date', current_time( 'm/d/Y' ) ); - // check the lessons Drip Settings methods list contains 'start', - // as the course now has a start date set. - foreach ( $this->metabox->get_fields() as $index => $f ) { - if ( 'Drip Settings' === $f['title'] ) { - $this->assertTrue( array_key_exists( 'start', $f['fields'][0]['value'] ) ); - break; - } - } - - } - -} diff --git a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-details.php b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-details.php deleted file mode 100644 index 8bff9e368a..0000000000 --- a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-details.php +++ /dev/null @@ -1,229 +0,0 @@ -<?php -/** - * Tests for LifterLMS Order Metabox - * - * @package LifterLMS/Tests - * - * @group admin - * @group metaboxes - * @group order_details - * - * @since 5.3.0 - */ -class LLMS_Test_Meta_Box_Order_Details extends LLMS_PostTypeMetaboxTestCase { - - /** - * Setup test - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Meta_Box_Order_Details(); - - } - - /** - * Test save() nonce-related errors - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_errs_nonce() { - - // No nonce. - $this->assertEquals( -1, $this->main->save( 123 ) ); - - // Invalid nonce. - $this->mockPostRequest( $this->add_nonce_to_array( array(), false ) ); - $this->assertEquals( -1, $this->main->save( 123 ) ); - - } - - /** - * Test save() with an invalid order. - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_order_err() { - - $post_id = $this->factory->post->create(); - $this->mockPostRequest( $this->add_nonce_to_array( array() ) ); - - // Not an order post type. - $this->assertEquals( 0, $this->main->save( $post_id ) ); - - // Non-existent post id. - $this->assertEquals( 0, $this->main->save( ++$post_id ) ); - - } - - /** - * Test save() gateway data. - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_success_payment_gateway_data() { - - $updates = array( - 'payment_gateway' => 'mock_gateway', - 'gateway_customer_id' => 'cust_12345', - 'gateway_subscription_id' => 'sub_678', - 'gateway_source_id' => 'source_1011', - ); - - $post_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) ); - $this->mockPostRequest( $this->add_nonce_to_array( $updates ) ); - - $this->assertEquals( 1, $this->main->save( $post_id ) ); - - $order = llms_get_post( $post_id ); - foreach ( $updates as $key => $val ) { - $this->assertEquals( $val, $order->get( $key ) ); - } - - } - - /** - * Test save() when remaining payment data is updated - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_success_remaining_payment_data() { - - $order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) ); - $order = llms_get_post( $order_id ); - $order->set( 'order_type', 'recurring' ); - $order->set( 'billing_length', 5 ); - $order->set( 'billing_period', 'day' ); - - $this->mockPostRequest( $this->add_nonce_to_array( array( - '_llms_remaining_payments' => 3, - '_llms_remaining_note' => 'Mock note', - ) ) ); - - $this->main->save( $order_id ); - - // Data. - $this->assertEquals( 3, $order->get( 'billing_length' ) ); - $this->assertEquals( 3, $order->get_remaining_payments() ); - - // Notes. - remove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) ); - $notes = $order->get_notes(); - add_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) ); - - $user_note = array_pop( $notes ); - $this->assertEquals( 'Mock note', $user_note->comment_content ); - - $system_note = array_pop( $notes ); - $this->assertEquals( 'The billing length of the order has been modified from 5 days to 3 days.', $system_note->comment_content ); - - } - - /** - * Test save_remaining_payments() when no changes should occur. - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_remaining_payments_no_changes() { - - $order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) ); - $order = llms_get_post( $order_id ); - - // Single order. - $order->set( 'order_type', 'single' ); - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - // Recurring without expiration. - $order->set( 'order_type', 'recurring' ); - $order->set( 'billing_length', 0 ); - $this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - // Nothing to save: no update submitted. - $order->set( 'billing_length', 3 ); - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - // Update submitted with no change. - $this->mockPostRequest( array( - '_llms_remaining_payments' => $order->get_remaining_payments(), - ) ); - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - // Can't end a plan via an adjustment. - $this->mockPostRequest( array( - '_llms_remaining_payments' => 0, - ) ); - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - } - - /** - * Test save_remaining_payments() when changes are made. - * - * @since 5.3.0 - * - * @return void - */ - public function test_save_remaining_payments_success() { - - $order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) ); - $order = llms_get_post( $order_id ); - $order->set( 'order_type', 'recurring' ); - $order->set( 'billing_length', 5 ); - - // Has one payment. - $order->record_transaction( array( - 'payment_type' => 'recurring', - 'status' => 'llms-txn-succeeded', - ) ); - - // Reduce to one remaining payment. - $this->mockPostRequest( array( - '_llms_remaining_payments' => 1, - ) ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - $this->assertEquals( 2, $order->get( 'billing_length' ) ); - $this->assertEquals( 1, $order->get_remaining_payments() ); - - // Increase to 7 remaining. - $this->mockPostRequest( array( - '_llms_remaining_payments' => 7, - ) ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - $this->assertEquals( 8, $order->get( 'billing_length' ) ); - $this->assertEquals( 7, $order->get_remaining_payments() ); - - // Record another payment. - $order->record_transaction( array( - 'payment_type' => 'recurring', - 'status' => 'llms-txn-succeeded', - ) ); - - // Decrease to 3 remaining. - $this->mockPostRequest( array( - '_llms_remaining_payments' => 3, - ) ); - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) ); - - $this->assertEquals( 5, $order->get( 'billing_length' ) ); - $this->assertEquals( 3, $order->get_remaining_payments() ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-enrollment.php b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-enrollment.php deleted file mode 100644 index 5a4468b412..0000000000 --- a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-enrollment.php +++ /dev/null @@ -1,222 +0,0 @@ -<?php -/** - * Tests for LifterLMS Order Metabox - * - * @package LifterLMS/Tests - * - * @group admin - * @group metaboxes - * @group LLMS_Meta_Box_Order_Enrollment - * - * @since 3.33.0 - * @since 4.18.0 Added some tests on the output method. - * @version 4.18.0 - */ -class LLMS_Test_Meta_Box_Order_Enrollment extends LLMS_PostTypeMetaboxTestCase { - - /** - * Setup test - * - * @since 3.33.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->metabox = new LLMS_Meta_Box_Order_Enrollment(); - - } - - /** - * Test the LLMS_Meta_Box_Order_Enrollment save method - * - * @since 3.33.0 - * - * @return void - */ - public function test_save() { - - // Create a real order. - $order = $this->get_mock_order(); - - $order_id = $order->get( 'id' ); - $product_id = $order->get( 'product_id' ); - $student_id = $order->get( 'user_id' ); - - // Check enroll. - $this->setup_post( array( - 'llms_update_enrollment_status' => 'Update', - 'llms_student_old_enrollment_status' => '', - 'llms_student_new_enrollment_status' => 'enrolled', - ) ); - - $this->metabox->save( $order_id ); - $this->assertTrue( llms_is_user_enrolled( $student_id, $product_id ) ); - - // Check unenroll. - $this->setup_post( array( - 'llms_update_enrollment_status' => 'Update', - 'llms_student_old_enrollment_status' => 'enrolled', - 'llms_student_new_enrollment_status' => 'expired', - ) ); - - $this->metabox->save( $order_id ); - $this->assertFalse( llms_is_user_enrolled( $student_id, $product_id ) ); - - // Check enrollment deleted => no enrollment records + order status set to cancelled. - $this->setup_post( array( - 'llms_delete_enrollment_status' => 'Delete', - 'llms_student_old_enrollment_status' => 'expired', - 'llms_student_new_enrollment_status' => 'deleted', - ) ); - - $this->metabox->save( $order_id ); - $this->assertFalse( llms_is_user_enrolled( $student_id, $product_id ) ); - $this->assertEquals( array(), llms_get_user_postmeta( $student_id, $product_id ) ); - $this->assertSame( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) ); - - } - - - /** - * Test the LLMS_Meta_Box_Order_Enrollment output method for anonymized orders - * - * @since 4.18.0 - * - * @return void - */ - public function test_output_anonymized_order() { - - // Create a real order. - $order = $this->get_mock_order(); - - $order_id = $order->get( 'id' ); - $product_id = $order->get( 'product_id' ); - $student_id = $order->get( 'user_id' ); - - $order->set( 'anonymized', 'yes' ); - - $this->assertOutputEquals( 'Cannot manage enrollment status for anonymized orders.', array( $this->metabox, 'output' ) ); - - } - - /** - * Test the LLMS_Meta_Box_Order_Enrollment output method for orders with no user - * - * @since 4.18.0 - * - * @return void - */ - public function test_output_order_with_no_user() { - - // Create a real order. - $order = $this->get_mock_order(); - - $order_id = $order->get( 'id' ); - $product_id = $order->get( 'product_id' ); - - $order->set( 'user_id', '' ); - $this->assertOutputEmpty( array( $this->metabox, 'output' ) ); - - } - - - /** - * Test the LLMS_Meta_Box_Order_Enrollment output method for orders of deleted students - * - * @since 4.18.0 - * - * @return void - */ - public function test_output_order_with_deleted_student() { - - // Create a real order. - $order = $this->get_mock_order(); - - $order_id = $order->get( 'id' ); - $product_id = $order->get( 'product_id' ); - $student_id = $order->get( 'user_id' ); - - wp_delete_user( $student_id ); - - $this->assertOutputEquals( "The student who placed the order doesn't exist anymore.", array( $this->metabox, 'output' ) ); - - } - - /** - * Test the LLMS_Meta_Box_Order_Enrollment output method for orders with student - * - * @since 4.18.0 - * - * @return void - */ - public function test_output_order_with_student() { - - // Create a real order. - $order = $this->get_mock_order(); - - $order_id = $order->get( 'id' ); - $product_id = $order->get( 'product_id' ); - $student_id = $order->get( 'user_id' ); - - $output = $this->get_output( array( $this->metabox, 'output' ) ); - - // There's a status selecter. - $this->assertStringContainsString( '<select name="llms_student_new_enrollment_status">', $output ); - - // The student is not enrolled yet. - // No selected option, as well as the old (current) enrollment status. - $this->assertStringNotContainsString( "selected='selected'>", $output ); - $this->assertStringContainsString( '<input name="llms_student_old_enrollment_status" type="hidden" value="">', $output ); - // The delete enrollment button doesn't exist. - $this->assertStringNotContainsString( '<input name="llms_delete_enrollment_status" ', $output ); - - // Enroll the student. - llms_enroll_student( $student_id, $product_id ); - - $output = $this->get_output( array( $this->metabox, 'output' ) ); - - // The selected option is 'enrolled', as well as the old (current) enrollment status. - $this->assertStringContainsString( "<option value=\"enrolled\" selected='selected'>", $output ); - $this->assertStringContainsString( '<input name="llms_student_old_enrollment_status" type="hidden" value="enrolled">', $output ); - // The delete enrollment button does not exist. - $this->assertStringNotContainsString( '<input name="llms_delete_enrollment_status" ', $output ); - - // Unenroll the student (cancelled status). - llms_unenroll_student( $student_id, $product_id, 'cancelled', 'any' ); - - $output = $this->get_output( array( $this->metabox, 'output' ) ); - - // The selected option is 'cancelled', as well as the old (current) enrollment status. - $this->assertStringContainsString( "<option value=\"cancelled\" selected='selected'>", $output ); - $this->assertStringContainsString( '<input name="llms_student_old_enrollment_status" type="hidden" value="cancelled">', $output ); - // The delete enrollment button exists. - $this->assertStringContainsString( '<input name="llms_delete_enrollment_status" ', $output ); - - // Unenroll the student (expired status). - llms_enroll_student( $student_id, $product_id ); - llms_unenroll_student( $student_id, $product_id, 'expired', 'any' ); - - $output = $this->get_output( array( $this->metabox, 'output' ) ); - - // The selected option is 'expired', as well as the old (current) enrollment status. - $this->assertStringContainsString( "<option value=\"expired\" selected='selected'>", $output ); - $this->assertStringContainsString( '<input name="llms_student_old_enrollment_status" type="hidden" value="expired">', $output ); - // The delete enrollment button exists. - $this->assertStringContainsString( '<input name="llms_delete_enrollment_status" ', $output ); - - // Delete enrollment. - llms_delete_student_enrollment( $student_id, $product_id, 'any' ); - - $output = $this->get_output( array( $this->metabox, 'output' ) ); - - // No selected option, as well as the old (current) enrollment status. - $this->assertStringNotContainsString( "selected='selected'>", $output ); - $this->assertStringContainsString( '<input name="llms_student_old_enrollment_status" type="hidden" value="">', $output ); - // The delete enrollment button doesn't exist. - $this->assertStringNotContainsString( '<input name="llms_delete_enrollment_status" ', $output ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-accounts.php b/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-accounts.php deleted file mode 100644 index e4131568d3..0000000000 --- a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-accounts.php +++ /dev/null @@ -1,132 +0,0 @@ -<?php -/** - * Test LLMS_Settings_Accounts - * - * @package LifterLMS/Tests - * - * @group admin - * @group settings_page - * @group settings_page_accounts - * - * @since 3.37.3 - * @since 3.37.4 The ID is "account" not "accounts". - */ -class LLMS_Test_Settings_Accounts extends LLMS_Settings_Page_Test_Case { - - /** - * Classname. - * - * @var string - */ - protected $classname = 'LLMS_Settings_Accounts'; - - /** - * Expected class $id property. - * - * @var string - */ - protected $class_id = 'account'; - - /** - * Expected class $label property. - * - * @var string - */ - protected $class_label = 'Accounts'; - - /** - * Return an array of mock settings and possible values. - * - * @since 3.37.3 - * - * @return void - */ - protected function get_mock_settings() { - - $pages = array( - $this->factory->post->create( array( 'post_type' => 'page' ) ), - $this->factory->post->create( array( 'post_type' => 'page' ) ), - ); - - return array( - 'lifterlms_myaccount_page_id' => $pages, - 'lifterlms_myaccount_courses_in_progress_sorting' => array( - 'title,ASC', - 'title,DESC', - 'date,DESC', - 'order,ASC', - 'order,DESC', - ), - 'lifterlms_enable_myaccount_registration' => array( - 'yes', - ), - 'lifterlms_prevent_concurrent_logins' => array( - 'yes', - ), - 'lifterlms_prevent_concurrent_logins_roles' => array( - array( '' ), - array( 'student' ), - ), - 'lifterlms_myaccount_grades_endpoint' => array( - 'my-grades', - 'custom-endpoint-grades', - ), - 'lifterlms_myaccount_courses_endpoint' => array( - 'my-courses', - 'custom-endpoint-courses', - ), - 'lifterlms_myaccount_memberships_endpoint' => array( - 'my-memberships', - 'custom-endpoint-memberships', - ), - 'lifterlms_myaccount_achievements_endpoint' => array( - 'my-achievements', - 'custom-endpoint-achievements', - ), - 'lifterlms_myaccount_certificates_endpoint' => array( - 'my-certificates', - 'custom-endpoint-certificates', - ), - 'lifterlms_myaccount_notifications_endpoint' => array( - 'notifications', - 'custom-endpoint-notifications', - ), - 'lifterlms_myaccount_edit_account_endpoint' => array( - 'edit-account', - 'custom-endpoint-account', - ), - 'lifterlms_myaccount_lost_password_endpoint' => array( - 'lost-password', - 'custom-endpoint-reset-pass', - ), - 'lifterlms_myaccount_redeem_vouchers_endpoint' => array( - 'redeem-voucher', - 'custom-redemption-code' - ), - 'lifterlms_myaccount_orders_endpoint' => array( - 'orders', - 'custom-order-history', - ), - 'lifterlms_registration_require_agree_to_terms' => array( - 'yes', - ), - 'lifterlms_terms_page_id' => $pages, - 'llms_terms_notice' => array( - llms_get_terms_notice(), - 'mock terms notice', - ), - 'wp_page_for_privacy_policy' => $pages, - 'llms_privacy_notice' => array( - llms_get_privacy_notice(), - 'mock privacy notice', - ), - 'llms_erasure_request_removes_order_data' => array( - 'yes', - ), - 'llms_erasure_request_removes_lms_data' => array( - 'yes', - ), - ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-engagements.php b/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-engagements.php deleted file mode 100644 index 10d16dc81a..0000000000 --- a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-engagements.php +++ /dev/null @@ -1,129 +0,0 @@ -<?php -/** - * Test LLMS_Settings_Engagements - * - * @package LifterLMS/Tests - * - * @group admin - * @group settings_page - * @group settings_page_engagements - * - * @since 3.37.3 - * @since 3.40.0 Add tests for `get_settings_group_email_delivery()`. - */ -class LLMS_Test_Settings_Engagements extends LLMS_Settings_Page_Test_Case { - - /** - * Classname. - * - * @var string - */ - protected $classname = 'LLMS_Settings_Engagements'; - - /** - * Expected class $id property. - * - * @var string - */ - protected $class_id = 'engagements'; - - /** - * Expected class $label property. - * - * @var string - */ - protected $class_label = 'Engagements'; - - /** - * Return an array of mock settings and possible values. - * - * @since 3.37.3 - * - * @return void - */ - protected function get_mock_settings() { - - return array( - 'lifterlms_email_from_name' => array( - esc_attr( get_bloginfo( 'title' ) ), - 'mock from name', - ), - 'lifterlms_email_from_address' => array( - get_option( 'admin_email' ), - 'mock@mock.com', - ), - 'lifterlms_email_header_image' => array( - 'fake.png', - ), - 'lifterlms_email_footer_text' => array( - 'footer text content', - ), - 'lifterlms_certificate_bg_img_width' => array( - 800, - 1024, - ), - 'lifterlms_certificate_bg_img_height' => array( - 616, - 1200, - ), - 'lifterlms_certificate_legacy_image_size' => array( - 'yes', - ), - ); - - } - - /** - * Retrieve mock email provider settings used to test the get_settings_group_email_delivery() method. - * - * @since 3.40.0 - * - * @return array[] - */ - public function get_mock_email_provider_settings() { - - return array( - array( - 'id' => 'mock_email_provider_title', - 'type' => 'subtitle', - 'title' => 'Email sender', - ), - ); - - } - - /** - * Return an array of mock settings and possible values. - * - * @since 3.40.0 - * - * @return void - */ - public function test_get_settings_group_email_delivery_no_providers() { - - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' ) ); - - } - - /** - * Return an array of mock settings and possible values. - * - * @since 3.40.0 - * - * @return void - */ - public function test_get_settings_group_email_delivery_with_providers() { - - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' ) ); - - add_filter( 'llms_email_delivery_services', array( $this, 'get_mock_email_provider_settings' ) ); - - $res = LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' ); - - $this->assertEquals( array( 'email_delivery', 'email_delivery_title', 'mock_email_provider_title', 'email_delivery_end' ), wp_list_pluck( $res, 'id' ) ); - - remove_filter( 'llms_email_delivery_services', array( $this, 'get_mock_email_provider' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-page.php b/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-page.php deleted file mode 100644 index ed0f6f4a8c..0000000000 --- a/tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-page.php +++ /dev/null @@ -1,350 +0,0 @@ -<?php -/** - * Test LLMS_Settings_Page - * - * @package LifterLMS/Tests - * - * @group admin - * @group settings_page - * - * @since 3.37.3 - */ -class LLMS_Test_Settings_Page extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.37.3 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - include_once LLMS_PLUGIN_DIR . 'includes/admin/settings/class.llms.settings.page.php'; - - // Setup a mock settings page. - $this->page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - protected function set_label() { - return 'Mock'; - } - }; - - } - - /** - * Test constructor - * - * @since 3.37.3 - * - * @return void - */ - public function test_constructor() { - - $this->assertEquals( 'Mock', $this->page->label ); - - // Tab should be registered. - $this->assertEquals( $this->page->tab_priority, has_action( 'lifterlms_settings_tabs_array', array( $this->page, 'add_settings_page' ) ) ); - - // Output action. - $this->assertEquals( 10, has_action( 'lifterlms_settings_mock', array( $this->page, 'output' ) ) ); - - // Save action. - $this->assertEquals( 10, has_action( 'lifterlms_settings_save_mock', array( $this->page, 'save' ) ) ); - - } - - /** - * Test add_settings_page() method. - * - * @since 3.37.3 - * - * @return void - */ - public function test_add_settings_page() { - - // No other pages exist. - $this->assertEquals( array( - 'mock' => 'Mock', - ), $this->page->add_settings_page( array() ) ); - - // Another page exists, add it to the end. - $this->assertEquals( array( - 'fake' => 'Fake', - 'mock' => 'Mock', - ), $this->page->add_settings_page( array( - 'fake' => 'Fake', - ) ) ); - - } - - /** - * Test generation of a settings group with no settings added. - * - * @since 3.37.3 - * - * @return void - */ - public function test_generate_settings() { - - // No description. - $args = array( - 'mock_options', - 'Mock Settings', - ); - $settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args ); - - $this->assertEquals( 'mock_options', $settings[0]['id'] ); - $this->assertEquals( 'sectionstart', $settings[0]['type'] ); - - $this->assertEquals( 'mock_options_title', $settings[1]['id'] ); - $this->assertEquals( 'title', $settings[1]['type'] ); - $this->assertEquals( 'Mock Settings', $settings[1]['title'] ); - $this->assertEquals( '', $settings[1]['desc'] ); - - $this->assertEquals( 'mock_options_end', $settings[2]['id'] ); - $this->assertEquals( 'sectionend', $settings[2]['type'] ); - - // Has a description. - $args = array( - 'mock_options', - 'Mock Settings', - 'Mock Settings Description', - ); - $settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args ); - $this->assertEquals( 'Mock Settings Description', $settings[1]['desc'] ); - - // Has a description. - $args = array( - 'mock_options', - 'Mock Settings', - 'Mock Settings Description', - array( - array( - 'id' => 'mock_setting', - 'type' => 'text', - ), - ) - ); - $settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args ); - $this->assertEquals( 'mock_setting', $settings[2]['id'] ); - $this->assertEquals( 'text', $settings[2]['type'] ); - - } - - - /** - * Test set_label() stub when no ID exists for the class. - * - * @since 3.37.3 - * - * @return void - */ - public function test_set_label_stub_no_id() { - - // Empty string because no ID defined. - $page = new LLMS_Settings_Page(); - $this->assertEquals( '', LLMS_Unit_Test_Util::call_method( $page, 'set_label' ) ); - - } - - /** - * Test set_label() stub when an ID is set. - * - * @since 3.37.3 - * - * @return void - */ - public function test_set_label_stub_with_id() { - - // Return ID because the method isn't overriden. - $page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - }; - $this->assertEquals( 'mock', LLMS_Unit_Test_Util::call_method( $page, 'set_label' ) ); - - } - - /** - * Test the get_sections() stub. - * - * @since 3.37.3 - * - * @return void - */ - public function test_get_sections() { - $this->assertEquals( array(), $this->page->get_sections() ); - } - - /** - * Test the get_settings() stub. - * - * @since 3.37.3 - * - * @return void - */ - public function test_get_settings_stub() { - $this->assertEquals( array(), $this->page->get_settings() ); - } - - /** - * Test the output() stub. - * - * @since 3.37.3 - * - * @return void - */ - public function test_output() { - $this->assertOutputEmpty( array( $this->page, 'output' ) ); - } - - /** - * Test the output_sections_nav() stub when no sections exist. - * - * @since 3.37.3 - * - * @return void - */ - public function test_output_sections_nav_empty() { - $this->assertOutputEmpty( array( $this->page, 'output_sections_nav' ) ); - } - - /** - * Test the output_sections_nav() stub when sections do exist. - * - * @since 3.37.3 - * - * @return void - */ - public function test_output_sections_nav() { - - $page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - public function get_sections() { - return array( - 'section_1' => 'Section 1', - 'section_2' => 'Section 2', - ); - } - }; - - $method = array( $page, 'output_sections_nav' ); - - $this->assertOutputContains( '<nav class="llms-nav-tab-wrapper llms-nav-text">', $method ); - $this->assertOutputContains( '<ul class="llms-nav-items">', $method ); - - $this->assertOutputContains( 'section=section_1">Section 1</a>', $method ); - $this->assertOutputContains( 'section=section_2">Section 2</a>', $method ); - - $this->assertOutputContains( '</ul>', $method ); - $this->assertOutputContains( '</nav>', $method ); - - } - - /** - * Test the save() method. - * - * @since 3.37.3 - * - * @return void - */ - public function test_save() { - - $page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - public function get_settings() { - return array( - array( - 'id' => 'mock_setting_id', - 'type' => 'text', - ), - array( - 'id' => 'mock_setting_id_2', - 'type' => 'text', - ), - ); - } - }; - - // No data posted. - $page->save(); - $this->assertEmpty( get_option( 'mock_setting_id' ) ); - $this->assertEmpty( get_option( 'mock_setting_id_2' ) ); - - // Some Data posted. - $this->mockPostRequest( array( 'mock_setting_id' => 'mock_setting_val' ) ); - $page->save(); - $this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id' ) ); - $this->assertEmpty( get_option( 'mock_setting_id_2' ) ); - - // All Data posted. - $this->mockPostRequest( array( - 'mock_setting_id' => 'mock_setting_val', - 'mock_setting_id_2' => 'mock_setting_val', - ) ); - $page->save(); - $this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id' ) ); - $this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id_2' ) ); - - } - - /** - * Ensure unregistered (fake) options aren't stored during save events. - * - * @since 3.37.3 - * - * @return void - */ - public function test_save_fake_option() { - - // Fake option. - $this->mockPostRequest( array( - 'mock_setting_id_3' => 'mock_setting_val', - ) ); - $this->page->save(); - $this->assertEmpty( get_option( 'mock_setting_id_3' ) ); - - } - - /** - * Test the save() method when the $flush prop is true. - * - * @since 3.37.3 - * - * @return void - */ - public function test_save_flush_disabled() { - - $page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - }; - - $this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) ); - $page->save(); - $this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) ); - - } - - /** - * Test the save() method when the $flush prop is true. - * - * @since 3.37.3 - * - * @return void - */ - public function test_save_flush_enabled() { - - $page = new class() extends LLMS_Settings_Page { - public $id = 'mock'; - protected $flush = true; - }; - - $this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) ); - $page->save(); - $this->assertEquals( 10, has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-batch-eraser.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-batch-eraser.php deleted file mode 100644 index 2408129124..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-batch-eraser.php +++ /dev/null @@ -1,133 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Batch_Eraser class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group batch_eraser - * - * @since 3.37.19 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Batch_Eraser extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Batch_Eraser'; - - /** - * Teardown the test case. - * - * @since 3.37.19 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - $this->clear_cache(); - - } - - /** - * Clear cached batch count data. - * - * @since 3.37.19 - * - * @return void - */ - private function clear_cache() { - wp_cache_delete( 'batch-eraser', 'llms_tool_data' ); - } - - /** - * Test get_pending_batches() - * - * @since 3.37.19 - * - * @return void - */ - public function test_get_pending_batches() { - - $key = 'llms_background_processor_course_data_batch_ast9a0st'; - add_option( $key, array( 'data' ) ); - $this->clear_cache(); - - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) ); - - delete_option( $key ); - - } - - /** - * Test get_pending_batches(): no batches found. - * - * @since 3.37.19 - * - * @return void - */ - public function test_get_pending_batches_none_found() { - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) ); - } - - /** - * Test get_pending_batches(): when there's a cache hit. - * - * @since 3.37.19 - * - * @return void - */ - public function test_get_pending_batches_cache_hit() { - - wp_cache_set( 'batch-eraser', 25, 'llms_tool_data' ); - $this->assertEquals( 25, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) ); - - } - - /** - * Test handle() - * - * @since 3.37.19 - * - * @return void - */ - public function test_handle() { - - $key = 'llms_background_processor_course_data_batch_ast9a0st'; - add_option( $key, array( 'data' ) ); - $key = 'wp_llms_notification_processor_email_batch_ast9a0st'; - add_option( $key, array( 1, 2, 3 ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) ); - - $this->clear_cache(); - - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) ); - - } - - /** - * Test should_load() - * - * @since 3.37.19 - * - * @return void - */ - public function test_should_load() { - - $this->clear_cache(); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - wp_cache_set( 'batch-eraser', 25, 'llms_tool_data' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - } - - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-clear-sessions.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-clear-sessions.php deleted file mode 100644 index 5b0a89b071..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-clear-sessions.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Clear_Sessions class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group clear_sessions - * - * @since 4.0.0 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Clear_Sessions extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Clear_Sessions'; - - /** - * Test handle() - * - * @since 4.0.0 - * - * @return void - */ - public function test_handle() { - - $this->create_mock_session_data(); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) ); - - global $wpdb; - $this->assertEquals( 0, $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_sessions" ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-install-forms.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-install-forms.php deleted file mode 100644 index 420458005a..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-install-forms.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Install_Forms class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group install_forms - * - * @since 5.0.0 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Install_Forms extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Install_Forms'; - - /** - * Retrieve a list of core reusable block post ids - * - * @since 5.0.0 - * - * @return int[] - */ - private function get_block_posts() { - - $blocks = new WP_Query( array( - 'post_type' => 'wp_block', - 'meta_key' => '_llms_field_id', - 'meta_compare' => 'EXISTS', - ) ); - return wp_list_pluck( $blocks->posts, 'ID' ); - - } - - /** - * Retrieve a list of LLMS Form post objects - * - * @since 5.0.0 - * - * @return WP_Post[] - */ - private function get_form_posts() { - - $forms = new WP_Query( array( 'post_type' => 'llms_form' ) ); - return $forms->posts; - - } - - /** - * Test get_reusable_blcoks() - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_reusable_blocks() { - - LLMS_Forms::instance()->install(); - - $list = $this->main->get_reusable_blocks(); - - foreach ( $list as $id ) { - $this->assertTrue( is_numeric( $id ) ); - $block = get_post( $id ); - $this->assertEquals( 'wp_block', $block->post_type ); - $this->assertStringContains( '(Reusable)', $block->post_title ); - $this->assertNotEmpty( get_post_meta( $id, '_llms_field_id', true ) ); - } - - } - - /** - * Test handle() - * - * @since 5.0.0 - * - * @return void - */ - public function test_handle() { - - LLMS_Forms::instance()->install(); - - foreach ( $this->get_form_posts() as $form ) { - wp_update_post( array( - 'ID' => $form->ID, - 'post_content' => 'overwritten', - ) ); - } - - $original_blocks = $this->get_block_posts(); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) ); - - foreach ( $this->get_form_posts() as $form ) { - $this->assertNotEquals( 'overwritten', $form->post_content ); - } - - $new_blocks = $this->get_block_posts(); - $this->assertNotEmpty( $new_blocks ); - foreach ( $original_blocks as $id ) { - $this->assertFalse( in_array( $id, $new_blocks, true ) ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-limited-billing-order-locator.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-limited-billing-order-locator.php deleted file mode 100644 index 11add221dd..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-limited-billing-order-locator.php +++ /dev/null @@ -1,267 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Limited_Billing_Order_Locator class. - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group limited_billing - * - * @since 5.3.0 - * @version 5.4.0 - */ -class LLMS_Test_Admin_Tool_Limited_Billing_Order_Locator extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Limited_Billing_Order_Locator'; - - /** - * Teardown the test case. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - parent::tear_down(); - $this->clear_cache(); - } - - /** - * Clear cached tool data. - * - * @since 5.3.0 - * - * @return void - */ - private function clear_cache() { - wp_cache_delete( 'limited-billing-order-locator', 'llms_tool_data' ); - } - - /** - * Create mock orders. - * - * @since 5.3.0 - * - * @param int $count Number of orders. - * @param array $meta Order meta data. - * @param array $args Additional args. - * @return int[] - */ - private function create_mock_orders( $count, $meta = array(), $args = array() ) { - - return $this->factory->post->create_many( $count, wp_parse_args( $args, array( - 'post_type' => 'llms_order', - 'post_status' => 'llms-active', - 'meta_input' => $meta, - ) ) ); - - } - - /** - * Test generate_csv(). - * - * @since 5.3.0 - * - * @return void - */ - public function test_generate_csv() { - - $this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) ); - - // Not qualifying. - $this->create_mock_orders( 1 ); - $this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) ); - - // Has length but wrong status. - $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05' ), array( 'post_status' => 'llms-cancelled' ) ); - $this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) ); - - // Qualifying. - $this->create_mock_orders( 2, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) ); - $this->assertEquals( 2, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) ); - - } - - /** - * Test get_order_csv(): doesn't quality because the order hasn't ended and there's no refunds. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_order_csv_not_ended_no_refunds() { - - $order = llms_get_post( $this->create_mock_orders( 1, array( '_llms_billing_length' => 2 ) )[0] ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) ); - - } - - /** - * Test get_order_csv(): Doesn't qualify because it has ended but has the expected number of payments. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_order_right_number_of_payments() { - - $order = llms_get_post( $this->create_mock_orders( 1, array( '_llms_billing_length' => 2 ) )[0] ); - $order->record_transaction(); - $order->record_transaction(); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) ); - - } - - /** - * Test get_order_csv(): Qualifies because it has ended and is missing a payment. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_order_missing_payments() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Post_Types::register_post_types(); - - $order_id = $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) )[0]; - $order = llms_get_post( $order_id ); - $expect = array( $order_id, 2, 1, 1, 0, get_edit_post_link( $order_id, 'raw' ) ); - $order->record_transaction(); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) ); - - } - - /** - * Test get_order_csv(): Qualifies because it has a refund. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_order_has_refund() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Post_Types::register_post_types(); - - $order_id = $this->create_mock_orders( 1, array( '_llms_billing_length' => 5 ) )[0]; - $order = llms_get_post( $order_id ); - $expect = array( $order_id, 5, 1, 0, 1, get_edit_post_link( $order_id, 'raw' ) ); - $order->record_transaction( array( 'status' => 'llms-txn-refunded' ) ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) ); - - } - - /** - * Test get_csv() when there's nothing cached. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_csv_cache_miss() { - - $this->clear_cache(); - - $this->create_mock_orders( 2, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) ); - $expect = LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_csv' ) ); - - // Should be cached. - $this->assertEquals( $expect, wp_cache_get( 'limited-billing-order-locator', 'llms_tool_data' ) ); - - } - - /** - * Test get_csv() when there's cached results. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_csv_cache_hit() { - - wp_cache_set( 'limited-billing-order-locator', 'fake', 'llms_tool_data' ); - $this->assertEquals( 'fake', LLMS_Unit_Test_Util::call_method( $this->main, 'get_csv' ) ); - - } - - /** - * Test handle(). - * - * @since 5.3.0 - * @since 5.4.0 Made sure to compare the lists of orders with the same ordering. - * - * @return void - */ - public function test_handle() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Post_Types::register_post_types(); - - // Included. - $orders = $this->create_mock_orders( 3, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) ); - - // Not included bc it was created after the migration.. - $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) ); - - try { - - LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ); - - } catch ( LLMS_Unit_Test_Exception_Exit $exception ) { - - $csv = $exception->get_status(); - - $this->assertTrue( is_string( $csv ) ); - - $lines = explode( "\n", $csv ); - $this->assertEquals( '"Order ID","Expected Payments","Total Payments","Successful Payments","Refunded Payments","Edit Link"', $lines[0] ); - array_shift( $lines ); - $orders = array_reverse( $orders ); // Orders affected by the change ($lines) are ordered by their `ID` `DESC`. - - foreach ( $lines as $i => $line ) { - // Empty line at the end of the file. - if ( 3 === $i ) { - $this->assertEmpty( $line ); - } else { - $link = get_edit_post_link( $orders[ $i ], 'raw' ); - $this->assertEquals( "{$orders[ $i ]},2,0,0,0,{$link}", $line ); - } - } - - } - - } - - /** - * Test should_load() - * - * @since 5.3.0 - * - * @return void - */ - public function test_should_load() { - - // Shouldn't load. - $this->create_mock_orders( 1 ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - // Created after upgrade, shouldn't load. - $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - // Should load. - $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-recurring-payment-rescheduler.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-recurring-payment-rescheduler.php deleted file mode 100644 index 48b569112b..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-recurring-payment-rescheduler.php +++ /dev/null @@ -1,224 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Recurring_Payment_Rescheduler class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group recurring_rescheduler - * - * @since 4.6.0 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Recurring_Payment_Rescheduler extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Recurring_Payment_Rescheduler'; - - /** - * Teardown the test case. - * - * @since 4.6.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - $this->clear_cache(); - - } - - /** - * Create N number of orders in the DB - * - * @since 4.6.0 - * - * @param integer $count Number of orders to create. - * @param boolean $remove_action Whether or not to remove a scheduled payment action. - * If `true`, creates orders that would be handled by the tool, otherwise creates orders - * that should be missed by the tool's queries. - * @return int[] An array of WP_Post IDs for the created orders. - */ - private function create_orders_to_handle( $count = 3, $remove_action = true ) { - - $orders = array(); - - $i = 1; - while ( $i <= $count ) { - - $order = $this->get_mock_order(); - $order->set_status( 'llms-active' ); - $order->maybe_schedule_payment(); - - if ( $remove_action ) { - $order->unschedule_recurring_payment(); - } - - $orders[] = $order->get( 'id' ); - - ++$i; - } - - return $orders; - - } - - /** - * Clear cached batch count data. - * - * @since 4.6.0 - * - * @return void - */ - private function clear_cache() { - wp_cache_delete( 'recurring-payment-rescheduler', 'llms_tool_data' ); - wp_cache_delete( 'recurring-payment-rescheduler-total-results', 'llms_tool_data' ); - } - - /** - * Test get_orders() during a cache hit - * - * @since 4.6.0 - * - * @return void - */ - public function test_get_orders_cache_hit() { - - wp_cache_set( 'recurring-payment-rescheduler', 'mock cache', 'llms_tool_data' ); - $this->assertEquals( 'mock cache', LLMS_Unit_Test_Util::call_method( $this->main, 'get_orders' ) ); - - } - - /** - * Test get_orders() during a cache miss - * - * @since 4.6.0 - * - * @return void - */ - public function test_get_orders_cache_miss() { - - $orders = $this->create_orders_to_handle(); - - // Order IDs returned. - $this->assertEqualSets( $orders, LLMS_Unit_Test_Util::call_method( $this->main, 'get_orders' ) ); - - // Cache is set. - $this->assertEqualSets( $orders, wp_cache_get( 'recurring-payment-rescheduler', 'llms_tool_data' ) ); - - } - - /** - * Test handle() - * - * @since 4.6.0 - * - * @return void - */ - public function test_handle() { - - $orders = $this->create_orders_to_handle(); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ); - - // All expected orders were handled. - $this->assertEqualSets( $orders, $res ); - - // Cache erased. - $this->assertFalse( wp_cache_get( 'recurring-payment-rescheduler', 'llms_tool_data' ) ); - - foreach ( $res as $id ) { - - $order = llms_get_post( $id ); - - // Action is rescheduled. - $this->assertEquals( $order->get_next_payment_due_date( 'U' ), $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ); - - } - - } - - /** - * Test handle() properly handles "legacy" orders that don't have `plan_ended()` meta data. - * - * @since 4.7.0 - * - * @return void - */ - public function test_handle_orders_with_no_meta() { - - // Force a WP_Error to be returned by LLMS_Order::get_next_payment_due_date(). - add_filter( 'llms_order_calculate_next_payment_date', '__return_empty_string' ); - - $orders = $this->create_orders_to_handle( 1 ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ); - - // No orders handled. - $this->assertEquals( array(), $res ); - - // The missing metadata has been added by the tool. - $this->assertEquals( 'yes', llms_get_post( $orders[0] )->get( 'plan_ended' ) ); - - remove_filter( 'llms_order_calculate_next_payment_date', '__return_empty_string' ); - - } - - /** - * Test query_orders() - * - * @since 4.6.0 - * @since 4.7.0 Add an order with `plan_ended` meta that should be ignored and add tests for `FOUND_ROWS()` cached data. - * - * @return void - */ - public function test_query_orders() { - - // No orders. - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'query_orders' ) ); - - // Should be found. - $to_handle = $this->create_orders_to_handle(); - - // This order should not be in the returned array. - $to_ignore = $this->create_orders_to_handle( 1, false ); - - // Ignored because of `plan_ended` meta data. - $to_ignore_2 = $this->create_orders_to_handle( 1 ); - llms_get_post( $to_ignore_2[0] )->set( 'plan_ended', 'yes' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'query_orders' ); - - $this->assertEqualSets( $to_handle, wp_list_pluck( $res, 'ID' ) ); - - // Test FOUND_ROWS() cache data. - $this->assertEquals( 3, wp_cache_get( sprintf( 'recurring-payment-rescheduler-total-results', $this->id ), 'llms_tool_data' ) ); - - } - - /** - * Test should_load() - * - * @since 4.6.0 - * - * @return void - */ - public function test_should_load() { - - // No orders to handle. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - // Orders to handle. - $this->create_orders_to_handle( 1 ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-reset-automatic-payments.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-reset-automatic-payments.php deleted file mode 100644 index 50e22a4c9d..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-reset-automatic-payments.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Reset_Automatic_Payments class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group reset_payments - * - * @since 4.13.0 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Reset_Automatic_Payments extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Reset_Automatic_Payments'; - - /** - * Test handle() - * - * @since 4.13.0 - * - * @return void - */ - public function test_handle() { - - $actions = did_action( 'llms_site_clone_detected' ); - - // Get the original values of options to be cleared. - $orig_url = get_option( 'llms_site_url' ); - $orig_ignore = get_option( 'llms_site_url_ignore' ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', admin_url( 'admin.php?page=llms-status&tab=tools') ) ); - - try { - LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ); - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $this->assertEquals( '', get_option( 'llms_site_url' ) ); - $this->assertEquals( 'no', get_option( 'llms_site_url_ignore' ) ); - $this->assertEquals( ++$actions, did_action( 'llms_site_clone_detected' ) ); - - // Reset to the orig values. - update_option( 'llms_site_url', $orig_url ); - update_option( 'llms_site_url_ignore', $orig_ignore ); - - throw $exception; - - } - - } - - /** - * Test should_load() with no constants set. - * - * @since 4.13.0 - * - * @return void - */ - public function test_should_load() { - $this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - } - - /** - * Test should_load() with LLMS_SITE_IS_CLONE constant set. - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_should_load_with_site_clone_constant_set() { - define( 'LLMS_SITE_IS_CLONE', false ); - $this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - } - - /** - * Test should_load() with LLMS_SITE_FEATURE_RECURRING_PAYMENTS constant set. - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_should_load_with_recurring_payments_constant_set() { - define( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', false ); - $this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) ); - } - -} diff --git a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-wipe-legacy-account-options.php b/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-wipe-legacy-account-options.php deleted file mode 100644 index cb08f697e4..0000000000 --- a/tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-wipe-legacy-account-options.php +++ /dev/null @@ -1,143 +0,0 @@ -<?php -/** - * Tests for the LLMS_Admin_Tool_Wipe_Legacy_Account_Options class - * - * @package LifterLMS/Tests/Admins/Tools - * - * @group admin - * @group admin_tools - * @group legacy_opts - * - * @since 5.0.0 - * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests. - */ -class LLMS_Test_Admin_Tool_Wipe_Legacy_Account_Options extends LLMS_Admin_Tool_Test_Case { - - /** - * Name of the class being tested. - * - * @var sting - */ - const CLASS_NAME = 'LLMS_Admin_Tool_Wipe_Legacy_Account_Options'; - - const LEGACY_OPTIONS = array( - 'lifterlms_registration_generate_username', - 'lifterlms_registration_password_strength', - 'lifterlms_registration_password_min_strength', - 'lifterlms_user_info_field_names_checkout_visibility', - 'lifterlms_user_info_field_address_checkout_visibility', - 'lifterlms_user_info_field_phone_checkout_visibility', - 'lifterlms_user_info_field_email_confirmation_checkout_visibility', - 'lifterlms_user_info_field_names_registration_visibility', - 'lifterlms_user_info_field_address_registration_visibility', - 'lifterlms_user_info_field_phone_registration_visibility', - 'lifterlms_user_info_field_email_confirmation_registration_visibility', - 'lifterlms_voucher_field_registration_visibility', - 'lifterlms_user_info_field_names_account_visibility', - 'lifterlms_user_info_field_address_account_visibility', - 'lifterlms_user_info_field_phone_account_visibility', - 'lifterlms_user_info_field_email_confirmation_account_visibility', - ); - - /** - * Tear Down - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - $this->delete_legacy_options(); - - } - - /** - * Test handle() - * - * @since 5.0.0 - * - * @return void - */ - public function test_handle() { - - global $wpdb; - - $sql = " - SELECT COUNT(*) FROM {$wpdb->options} - WHERE option_name IN (" . implode( ', ', array_fill( 0, count( self::LEGACY_OPTIONS ), '%s' ) ) . ')'; - - $query = $wpdb->prepare( - $sql, - self::LEGACY_OPTIONS - ); - - $this->assertEquals( 0, $wpdb->get_var( $query ) ); - - $this->add_legacy_options(); - - $this->assertEquals( count( self::LEGACY_OPTIONS ), $wpdb->get_var( $query ) ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) ); - - $this->assertEquals( 0, $wpdb->get_var( $query ) ); - - } - - /** - * Test can_load() - * - * @since 5.0.0 - * - * @return void - */ - public function test_should_load() { - $this->assertFalse( - LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) - ); - - $this->add_legacy_options(); - - $this->assertTrue( - LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) - ); - - // Check that the tool doesn't load after it has been handled. - LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ); - - $this->assertFalse( - LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) - ); - - } - - /** - * Add legacy options to the WP options table - * - * @since 5.0.0 - * - * @return void - */ - private function add_legacy_options() { - - array_map( 'add_option', self::LEGACY_OPTIONS, array_fill( 0, count( self::LEGACY_OPTIONS ), 'yes' ) ); - - } - - - /** - * Remove legacy options to the WP options table - * - * @since 5.0.0 - * - * @return void - */ - private function delete_legacy_options() { - - array_map( 'delete_option', self::LEGACY_OPTIONS, array_fill( 0, count( self::LEGACY_OPTIONS ), 'yes' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-coupons.php b/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-coupons.php deleted file mode 100644 index da00fb89e3..0000000000 --- a/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-coupons.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php -/** - * Test Coupon-related methods in the LLMS_AJAX_Handler class. - * - * @package LifterLMS/Tests/AJAX - * - * @group ajax_coupons - * @group ajax - * @group coupons - * - * @since 3.39.0 - */ -class LLMS_Test_AJAX_Handler_Coupons extends LLMS_UnitTestCase { - - /** - * Test remove_coupon_code() - * - * @since 3.39.0 - * - * @return void - */ - public function test_remove_coupon_code() { - - LLMS()->session->set( 'llms_coupon', 'this-will-be-cleared' ); - - $res = LLMS_AJAX_Handler::remove_coupon_code( array( - 'plan_id' => $this->get_mock_plan(), - ) ); - - // HTML returned. - $this->assertEquals( array( 'coupon_html', 'gateways_html', 'summary_html' ), array_keys( $res ) ); - - $this->assertFalse( LLMS()->session->get( 'llms_coupon' ) ); - - } - - /** - * Test validate_coupon_code(): no coupon data supplied - * - * @since 3.39.0 - * - * @return void - */ - public function test_validate_coupon_code_none_supplied() { - - $request = array(); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertWPErrorMessageEquals( 'Please enter a coupon code.', $res ); - - } - - /** - * Test validate_coupon_code(): no access plan supplied - * - * @since 3.39.0 - * - * @return void - */ - public function test_validate_coupon_code_no_plan() { - - $request = array( - 'code' => 123, - ); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertWPErrorMessageEquals( 'Please enter a plan ID.', $res ); - - } - - /** - * Test validate_coupon_code(): coupon not found - * - * @since 3.39.0 - * - * @return void - */ - public function test_validate_coupon_code_not_found() { - - $request = array( - 'code' => 123, - 'plan_id' => 456, - ); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertWPErrorMessageEquals( 'Coupon code "123" not found.', $res ); - - } - - /** - * Test validate_coupon_code(): coupon invalid for the given plan - * - * @since 3.39.0 - * - * @return void - */ - public function test_validate_coupon_code_invalid() { - - $coupon = new LLMS_Coupon( 'new', 'couponname' ); - $coupon->set( 'status', 'publish' ); - $coupon->set( 'coupon_courses', array( $this->factory->post->create() ) ); - - $request = array( - 'code' => 'couponname', - 'plan_id' => $this->get_mock_plan(), - ); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertStringContains( 'This coupon cannot be used to purchase', $res->get_error_message() ); - - } - - /** - * Test validate_coupon_code(): coupon code is valid - * - * @since 3.39.0 - * - * @return void - */ - public function test_validate_coupon_code_valid() { - - $coupon = new LLMS_Coupon( 'new', 'couponname' ); - $coupon->set( 'status', 'publish' ); - - $request = array( - 'code' => 'couponname', - 'plan_id' => $this->get_mock_plan(), - ); - - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - - // HTML returned. - $this->assertEquals( array( 'code', 'coupon_html', 'gateways_html', 'summary_html' ), array_keys( $res ) ); - - // Session data set. - $expect = array( - 'plan_id' => $request['plan_id'], - 'coupon_id' => $coupon->get( 'id' ), - ); - $this->assertEquals( $expect, LLMS()->session->get( 'llms_coupon' ) ); - - } - - /** - * Test validate_coupon_code(): prevent reflected xss - * - * Input is only a tag that will be stripped resulting in an empty response. - * - * @since 4.21.1 - * - * @return void - */ - public function test_validate_coupon_code_sanitization_empty_result() { - - $request = array( - 'code' => '<img src="#">', - ); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertWPErrorMessageEquals( 'Please enter a coupon code.', $res ); - - } - - /** - * Test validate_coupon_code(): prevent reflected xss - * - * Input is text mixed with a a tag that will be stripped resulting in a not found error. - * - * @since 4.21.1 - * - * @return void - */ - public function test_validate_coupon_code_sanitization_mixed_result() { - - $request = array( - 'code' => 'FAKE_CODE<script>alert(1);</script>_WITH_TAGS', - 'plan_id' => 123, - ); - $res = LLMS_AJAX_Handler::validate_coupon_code( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertWPErrorMessageEquals( 'Coupon code "FAKE_CODE_WITH_TAGS" not found.', $res ); - - } - - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-ajax-handler.php b/tests/phpunit/unit-tests/class-llms-test-ajax-handler.php deleted file mode 100644 index 539543f041..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-ajax-handler.php +++ /dev/null @@ -1,672 +0,0 @@ -<?php -/** - * Test AJAX Handler - * - * @package LifterLMS/Tests - * - * @group AJAX - * - * @since 3.32.0 - * @since 3.37.2 Added tests on querying courses/memberships filtererd by instructors. - * @since 3.37.14 Added tests on persisting tracking events. - * @since 3.37.15 Added tests for admin table methods. - * @since 5.5.0 Added tests on select2_query_posts when searching terms with quotes. - */ -class LLMS_Test_AJAX_Handler extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/class.llms.admin.reporting.php'; - } - - /** - * Setup the test - * - * @since 3.32.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - add_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 ); - } - - /** - * Teardown the test - * - * @since 3.32.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - parent::tear_down(); - remove_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 ); - } - - /** - * Call a method for the LLMS_AJAX_Handler class that calls wp_die() - * - * @since 3.32.0 - * - * @param string $function Method name. - * @param array $args $_REQUEST args. - * @return array - */ - protected function do_ajax( $function, $args = array() ) { - - ob_start(); - $this->mockPostRequest( $args ); - try { - call_user_func( array( 'LLMS_AJAX_Handler', $function ) ); - } catch ( WPAjaxDieContinueException $e ) {} - return json_decode( $this->last_response, true ); - - } - - /** - * Test export_admin_table() - * - * @since 3.37.15 - * - * @return void - */ - public function test_export_admin_table() { - - $expected_keys = array( 'filename', 'progress', 'url' ); - foreach( array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ) as $role ) { - wp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) ); - $res = LLMS_AJAX_Handler::export_admin_table( array( 'handler' => 'Students' ) ); - $this->assertEquals( $expected_keys, array_keys( $res ) ); - } - - } - - /** - * Test export_admin_table() with invalid handlers - * - * @since 3.37.15 - * - * @return void - */ - public function test_export_admin_table_invalid_handler() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // No handler. - $this->assertFalse( LLMS_AJAX_Handler::export_admin_table( array() ) ); - - // Invalid handler. - $this->assertFalse( LLMS_AJAX_Handler::export_admin_table( array( 'handler' => 'fake' ) ) ); - - } - - /** - * Test export_admin_table() ensuring only users with proper permissions can access. - * - * @since 3.37.15 - * - * @return void - */ - public function test_export_admin_table_invalid_permissions() { - - // No user. - $this->assertFalse( LLMS_AJAX_Handler::export_admin_table( array( 'handler' => 'Students' ) ) ); - - // Student. - wp_set_current_user( $this->factory->student->create() ); - $this->assertFalse( LLMS_AJAX_Handler::export_admin_table( array( 'handler' => 'Students' ) ) ); - - } - - /** - * Test get_admin_table_data() - * - * @since 3.37.15 - * - * @return void - */ - public function test_get_admin_table_data() { - - $expected_keys = array( 'args', 'thead', 'tbody', 'tfoot' ); - - foreach( array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ) as $role ) { - - wp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) ); - $res = LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) ); - $this->assertEquals( $expected_keys, array_keys( $res ) ); - - } - - } - - /** - * Test get_admin_table_data() when invalid handlers are submitted. - * - * @since 3.37.15 - * - * @return void - */ - public function test_get_admin_table_data_invalid_handler() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // No handler. - $this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array() ) ); - - // Invalid handler. - $this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'fake' ) ) ); - - } - - /** - * Test get_admin_table_data() ensuring only users with proper permissions can access. - * - * @since 3.37.15 - * - * @return void - */ - public function test_get_admin_table_data_invalid_permissions() { - - // No user. - $this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) ) ); - - // Student. - wp_set_current_user( $this->factory->student->create() ); - $this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) ) ); - - } - - /** - * Test the select2_query_posts() ajax method. - * - * @since 3.32.0 - * @since 3.37.2 Added tests on querying courses/memberships filtererd by instructors. - * - * @return void - */ - public function test_select2_query_posts() { - - $args = array( - 'post_type' => 'course', - ); - - // No results. - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 0, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $res['more'] ); - - $this->factory->post->create_many( 50, array( - 'post_type' => 'course', - ) ); - - // Full result list. - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 30, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertTrue( $res['more'] ); - - // Second page. - $args['page'] = 1; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 20, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $res['more'] ); - - // Term not found. - unset( $args['page'] ); - $args['term'] = 'arstarstarst'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 0, count( $res['items'] ) ); - - // Term found. - $args['term'] = 'title'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertTrue( count( $res['items'] ) >= 1 ); - - $this->factory->post->create_many( 5, array( - 'post_title' => 'search title', - ) ); - $this->factory->post->create_many( 5, array( - 'post_type' => 'course', - 'post_title' => 'search title', - ) ); - - // multiple post types. - $args['post_type'] .= ',post'; - $args['term'] = 'search title'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertTrue( array_key_exists( 'post', $res['items'] ) ); - $this->assertSame( 'Posts', $res['items']['post']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) ); - $this->assertTrue( array_key_exists( 'course', $res['items'] ) ); - $this->assertSame( 'Courses', $res['items']['course']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) ); - - // No results, when querying for 'future' posts. - $args = array( - 'post_type' => 'course', - 'post_statuses' => 'future', - ); - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 0, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $res['more'] ); - - // create 4 courses in draft. - $this->factory->post->create_many( 4, array( - 'post_type' => 'course', - 'post_status' => 'draft', - )); - - // 4 results when querying for Courses in 'draft'. - $args['post_statuses'] = 'draft'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 4, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $res['more'] ); - - // Full result list querying for 'draft' and 'publish' Course statuses. - $args['post_statuses'] .= ',publish'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 30, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertTrue( $res['more'] ); - - // Second page querying for 'draft' and 'publish' Course statuses. - $args['page'] = 1; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 29, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $res['more'] ); - - $this->factory->post->create_many( 1, array( - 'post_title' => 'search title again', - 'post_status' => 'draft', - )); - $this->factory->post->create_many( 5, array( - 'post_type' => 'course', - 'post_title' => 'search title again', - )); - - // Search for multiple post types and multiple status. - // Only 1 post in 'draft' and 5 courses 'publish' must be found matching the 'term'. - unset( $args['page'] ); - - $args['post_type'] .= ',post'; - $args['term'] = 'search title again'; - - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertTrue( array_key_exists( 'post', $res['items'] ) ); - $this->assertSame( 'Posts', $res['items']['post']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) ); - $this->assertSame( 1, count( $res['items']['post']['items'] ) ); - $this->assertTrue( array_key_exists( 'course', $res['items'] ) ); - $this->assertSame( 'Courses', $res['items']['course']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) ); - $this->assertSame( 5, count( $res['items']['course']['items'] ) ); - - // Search for multiple post types only for the 'draft' status. - // Only 1 post in 'draft' and no courses must be found matching the 'term'. - $args['post_statuses'] = 'draft'; - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertTrue( array_key_exists( 'post', $res['items'] ) ); - $this->assertSame( 'Posts', $res['items']['post']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) ); - $this->assertSame( 1, count( $res['items']['post']['items'] ) ); - - // 2 Courses and 2 Memberships when querying for multiple post types limited to a specific instructor id. - // create and setup an instructor for the just created 2 Courses and 2 Memberships. - $instructor_id = $this->factory->instructor->create(); - foreach ( array( 'course', 'llms_membership' ) as $post_type ) { - $ids = $this->factory->post->create_many( 2, array( - 'post_type' => $post_type, - )); - foreach ( $ids as $id ) { - llms_get_post( $id )->instructors()->set_instructors( array( - array( - 'id' => $instructor_id, - ), - )); - } - } - - $args = array( - 'post_type' => 'course,llms_membership', - 'post_statuses' => 'publish', - 'instructor_id' => $instructor_id, - ); - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertTrue( array_key_exists( 'course', $res['items'] ) ); - $this->assertSame( 'Courses', $res['items']['course']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) ); - $this->assertSame( 2, count( $res['items']['course']['items'] ) ); - $this->assertTrue( array_key_exists( 'llms_membership', $res['items'] ) ); - $this->assertSame( 'Memberships', $res['items']['llms_membership']['label'] ); - $this->assertTrue( array_key_exists( 'items', $res['items']['llms_membership'] ) ); - $this->assertSame( 2, count( $res['items']['llms_membership']['items'] ) ); - - } - - /** - * Test the select2_query_posts() ajax method with search term and quotes. - * - * @since 5.5.0 - * - * @return void - */ - public function test_select2_query_posts_search_term_quote() { - - $course = $this->factory->post->create( array( - 'post_title' => 'search title with this quotes:\'" - :)', - 'post_type' => 'course', - 'post_stauts' => 'publish', - )); - - $args = array( - 'post_type' => 'course', - 'term' => 'search title with this quotes:\'', - ); - - $res = $this->do_ajax( 'select2_query_posts', $args ); - $this->assertSame( 1, count( $res['items'] ) ); - $this->assertTrue( $res['success'] ); - $this->assertSame( $course, (int) $res['items'][0]['id'] ); - $this->assertSame( 'search title with this quotes:\'" - :)' . " (ID# $course)", $res['items'][0]['name'] ); - - } - - /** - * Test the errors returned by the LLMS_AJAX_Handler::update_student_enrollment() method. - * - * @since 3.33.0 - * - * @return void - */ - public function test_update_student_enrollment_errors() { - - $request = array( - 'post_id' => 1, - 'status' => 'add', - ); - // Missing student_id. - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Missing required parameters', $res->get_error_message() ); - - $request = array( - 'student_id' => 1, - 'status' => 'add', - ); - // Missing post_id. - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Missing required parameters', $res->get_error_message() ); - - $request = array( - 'student_id' => 1, - 'post_id' => 1, - ); - // Missing status. - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Missing required parameters', $res->get_error_message() ); - - $request = array( - 'status' => 'add', - 'student_id' => 1, - 'post_id' => '', - ); - // Empty post_id ( or student_id, or status) value. - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Missing required parameters', $res->get_error_message() ); - - $request = array( - 'status' => 'enjoy', - 'student_id' => 1, - 'post_id' => 2, - ); - // status not in ('add', 'remove', 'delete'). - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Invalid status', $res->get_error_message() ); - - // create a student. - $student = $this->get_mock_student(); - $student_id = $student->get( 'id' ); - - // create a course. - $course_id = $this->generate_mock_courses( 1, 1, 3 )[0]; - - $request = array( - 'status' => 'add', - 'student_id' => $student_id, - 'post_id' => $course_id + 1, - ); - // 'add' failure: no course. - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Action "add" failed. Please try again', $res->get_error_message() ); - - // 'remove' failure: student not enrolled in a Course with ID as $course_id. - $request['status'] = 'remove'; - $request['post_id'] = $course_id; - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Action "remove" failed. Please try again', $res->get_error_message() ); - - // 'delete' failure: student not enrolled in a Course with ID as $course_id. - $request['status'] = 'delete'; - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( '400', $res ); - $this->assertSame( 'Action "delete" failed. Please try again', $res->get_error_message() ); - - } - - /** - * Test the update_student_enrollment() method can perform user's enrollment - * - * @since 3.33.0 - * - * @return void - */ - public function test_update_student_enrollment_enroll() { - - // create a student. - $student = $this->get_mock_student(); - $student_id = $student->get( 'id' ); - - // create a course. - $course_id = $this->generate_mock_courses( 1, 1, 3 )[0]; - - $request = array( - 'status' => 'add', - 'student_id' => $student_id, - 'post_id' => $course_id, - ); - - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertTrue( $res['success'] ); - $this->assertTrue( $student->is_enrolled( $course_id ) ); - - } - - /** - * Test the update_student_enrollment() method can perform user's unenrollment - * - * @since 3.33.0 - * - * @return void - */ - public function test_update_student_enrollment_unenroll() { - - // create a student. - $student = $this->get_mock_student(); - $student_id = $student->get( 'id' ); - - // create a course. - $course_id = $this->generate_mock_courses( 1, 1, 3 )[0]; - - // enroll the student in the course. - $student->enroll( $course_id ); - - $request = array( - 'status' => 'remove', - 'student_id' => $student_id, - 'post_id' => $course_id, - ); - - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertTrue( $res['success'] ); - $this->assertFalse( $student->is_enrolled( $course_id ) ); - - } - - /** - * Test the update_student_enrollment() method can perform user's enrollment deletion - * - * @since 3.33.0 - * - * @return void - */ - public function test_update_student_enrollment_delete() { - - // create a student. - $student = $this->get_mock_student(); - $student_id = $student->get( 'id' ); - - // create a course. - $course_id = $this->generate_mock_courses( 1, 1, 3 )[0]; - - // enroll the student in the course. - $student->enroll( $course_id ); - - $request = array( - 'status' => 'delete', - 'student_id' => $student_id, - 'post_id' => $course_id, - ); - - $res = LLMS_AJAX_Handler::update_student_enrollment( $request ); - $this->assertTrue( $res['success'] ); - $this->assertEquals( array(), llms_get_user_postmeta( $student_id, $course_id ) ); - - } - - /** - * Test `persist_tracking_events()` ajax callback. - * - * @since 3.37.14 - * - * @return void - */ - public function test_persist_tracking_events() { - - $request = array( - 'something' => 'what' - ); - - // missing tracking data. - $res = LLMS_AJAX_Handler::persist_tracking_events( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'error', $res ); - $this->assertSame( 'Missing tracking data.', $res->get_error_message() ); - - - // unauthorized, missing tracking nonce or user not logged in. - - // create nonce. - // check user not logged in. - $request = array( - 'llms-tracking' => json_encode( - array( - 'events' => array(), - 'nonce' => wp_create_nonce( 'llms-tracking' ), - ) - ), - ); - - $res = LLMS_AJAX_Handler::persist_tracking_events( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_events_tracking_unauthorized', $res ); - $this->assertSame( 'You\'re not allowed to store tracking events', $res->get_error_message() ); - - // log-in. check missing nonce. - wp_set_current_user(1); - $request = array( - 'llms-tracking' => json_encode( - array( - 'events' => array(), - ) - ), - ); - - $res = LLMS_AJAX_Handler::persist_tracking_events( $request ); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_events_tracking_unauthorized', $res ); - $this->assertSame( 'You\'re not allowed to store tracking events', $res->get_error_message() ); - - // persist events. - $request = array( - 'llms-tracking' => json_encode( - array( - 'events' => array( - array( - 'object_type' => 'user', - 'object_id' => 1, - 'event' => 'account.signon', - ), - array( - 'object_type' => 'user', - 'object_id' => 1, - 'event' => 'account.signoff', - ), - ), - 'nonce' => wp_create_nonce( 'llms-tracking' ), - ) - ), - ); - - $res = LLMS_AJAX_Handler::persist_tracking_events( $request ); - $this->assertTrue( $res['success'] ); - $events = ( new LLMS_Events_Query( array( - 'actor' => array(1) - ) ) )->get_events(); - - $this->assertEquals( 2, count( $events ) ); - - } - - /** - * Catch wp_die() called by ajax methods & store the output buffer contents for use later. - * - * @since 3.32.0 - * - * @param string $msg Die msg. - * @return void - */ - public function _wp_die_handler( $msg ) { - $this->last_response = ob_get_clean(); - throw new WPAjaxDieContinueException( $msg ); - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-assets.php b/tests/phpunit/unit-tests/class-llms-test-assets.php deleted file mode 100644 index a67b79968a..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-assets.php +++ /dev/null @@ -1,870 +0,0 @@ -<?php -/** - * Test LLMS_Assets - * - * @package LifterLMS/Tests - * - * @group assets - * - * @since 4.4.0 - */ -class LLMS_Test_Assets extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 4.4.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - } - - /** - * Teardown the test case. - * - * Dequeue and deregister all assets that may have been registered/enqueued during the test. - * - * @since 4.4.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'scripts' ) ) as $handle ) { - wp_dequeue_script( $handle ); - wp_deregister_script( $handle ); - } - - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'styles' ) ) as $handle ) { - wp_dequeue_style( $handle ); - wp_deregister_style( $handle ); - } - - } - - /** - * Test merging of defaults during construction - * - * @since 4.9.0 - * @since 5.5.0 Add `asset_file`. - * - * @return void - */ - public function test_default_merge() { - - $defaults = array( - // Base defaults shared by all asset types. - 'base' => array( - 'base_file' => 'Some/Custom/Plugin/File.php', - 'base_url' => 'https://mock.tld/wp-content/plugins/custom-plugin', - 'version' => '93.29.107', - 'suffix' => '.custom', - ), - // Script specific defaults. - 'script' => array( - 'translate' => true, // All scripts in this plugin are translated. - ), - ); - - $expected = array( - 'base' => array( - 'base_file' => 'Some/Custom/Plugin/File.php', - 'base_url' => 'https://mock.tld/wp-content/plugins/custom-plugin', - 'suffix' => '.custom', - 'dependencies' => array(), - 'version' => '93.29.107', - ), - 'script' => array( - 'path' => 'assets/js', - 'extension' => '.js', - 'in_footer' => true, - 'translate' => true, - 'asset_file' => false, - ), - 'style' => array( - 'path' => 'assets/css', - 'extension' => '.css', - 'media' => 'all', - 'rtl' => true, - ), - ); - - - $assets = new LLMS_Assets( 'mock-package-id', $defaults ); - $this->assertEquals( $expected, LLMS_Unit_Test_Util::get_private_property_value( $assets, 'defaults' ) ); - - } - - /** - * Test define() with script assets. - * - * @since 4.4.0 - * - * @return void - */ - public function test_define_scripts() { - - $scripts = array( - 'llms' => array( 'src' => 'mock' ), // Overwrite an existing script. - 'mock' => array( 'src' => 'mock' ), // Define a new one. - ); - - $res = $this->main->define( 'scripts', $scripts ); - - $this->assertEquals( $scripts['llms'], $res['llms'] ); - $this->assertEquals( $scripts['mock'], $res['mock'] ); - - } - - /** - * Test define() with style assets. - * - * @since 4.4.0 - * - * @return void - */ - public function test_define_styles() { - - $styles = array( - 'lifterlms' => array( 'src' => 'mock' ), // Overwrite an existing style. - 'mock' => array( 'src' => 'mock' ), // Define a new one. - ); - - $res = $this->main->define( 'styles', $styles ); - - $this->assertEquals( $styles['lifterlms'], $res['lifterlms'] ); - $this->assertEquals( $styles['mock'], $res['mock'] ); - - } - - /** - * Test define() with an invalid type. - * - * @since 4.4.0 - * - * @return void - */ - public function test_define_invalid_type() { - - $this->assertFalse( $this->main->define( 'fake', array() ) ); - - } - - /** - * Test enqueue_inline() - * - * @since 4.4.0 - * - * @return void - */ - public function test_enqueue_inline() { - - $this->assertEquals( 10, $this->main->enqueue_inline( 'mock-foot', 'console.log( 1 );', 'footer' ) ); - - // Already enqueued. - $this->assertEquals( 10, $this->main->enqueue_inline( 'mock-foot', 'console.log( 1 );', 'footer' ) ); - - // Priority automatically incremented. - $this->assertEquals( 10.01, $this->main->enqueue_inline( 'mock-foot-two', 'console.log( 1 );', 'footer' ) ); - - // Explicit priority. - $this->assertEquals( 25, $this->main->enqueue_inline( 'mock-head', 'console.log( 1 );', 'header', 25 ) ); - - $inline = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'inline' ); - $this->assertEquals( array( 'mock-foot', 'mock-foot-two', 'mock-head' ), array_keys( $inline ) ); - - foreach ( $inline as $def ) { - $this->assertEquals( array( 'handle', 'asset', 'location', 'priority' ), array_keys( $def ) ); - } - - } - - /** - * Test enqueue_script() for a defined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_enqueue_script_defined() { - - $this->assertAssetNotRegistered( 'script', 'llms' ); - - // Register and enqueue. - $this->assertTrue( $this->main->enqueue_script( 'llms' ) ); - - // Already registered. - $this->assertTrue( $this->main->enqueue_script( 'llms' ) ); - - } - - /** - * Test enqueue_script() for an undefined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_enqueue_script_undefined() { - - $this->assertFalse( $this->main->enqueue_script( 'fake-script' ) ); - - } - - /** - * Test enqueue_style() for a defined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_enqueue_style_defined() { - - $this->assertAssetNotRegistered( 'style', 'lifterlms-styles' ); - - // Register and enqueue. - $this->assertTrue( $this->main->enqueue_style( 'lifterlms-styles' ) ); - - // Already registered. - $this->assertTrue( $this->main->enqueue_style( 'lifterlms-styles' ) ); - - } - - /** - * Test enqueue_style() for an undefined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_enqueue_style_undefined() { - - $this->assertFalse( $this->main->enqueue_style( 'fake-style' ) ); - - } - - /** - * Test get() method. - * - * @since 4.4.0 - * - * @return void - */ - public function test_get() { - - $asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'llms' ) ); - - // Add the handle to the data array. - $this->assertEquals( 'llms', $asset['handle'] ); - $this->assertArrayHasKey( 'src', $asset ); - $this->assertEquals( 'llms-core', $asset['package_id'] ); - - } - - /** - * Test get() method for an asset with an asset.php file - * - * @since 5.5.0 - * - * @return void - */ - public function test_get_with_asset_file() { - - $definition = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'scripts' )['llms-addons']; - $asset_file = include LLMS_PLUGIN_DIR . 'assets/js/llms-admin-addons.asset.php'; - - $asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'llms-addons' ) ); - - $this->assertArrayHasKey( 'src', $asset ); - - $this->assertEquals( $asset_file['version'], $asset['version'] ); - $this->assertEqualSets( $asset_file['dependencies'], $asset['dependencies'] ); - - } - - /** - * Test get() method for an undefined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_undefined() { - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'style', 'undefined-style' ) ) ); - - } - - /** - * Test get() method for an asset which is defined with an empty array signifying that all asset values should be defaults. - * - * @since 4.4.1 - * - * @see https://github.com/gocodebox/lifterlms/issues/1313 - * - * @return version - */ - public function test_get_all_default_values() { - - add_filter( 'llms_get_style_asset_before_prep', function( $asset, $handle ) { - - if ( 'mock-style-with-all-defaults' === $handle ) { - $asset = array(); - } - - return $asset; - - }, 10, 2 ); - - $asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'style', 'mock-style-with-all-defaults' ) ); - - $this->assertTrue( is_array( $asset ) ); - $this->assertEquals( 'mock-style-with-all-defaults', $asset['handle'] ); - - } - - /** - * Test that adding an asset with a custom src will use the custom src instead of a generated one - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_custom_src() { - - add_filter( 'llms_get_script_asset_before_prep', function( $asset, $handle ) { - - if ( 'mock-script-custom-src' === $handle ) { - $asset = array( - 'file_slug' => 'mock', - 'src' => 'custom-src', - ); - } - - return $asset; - - }, 10, 2 ); - - $asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'mock-script-custom-src' ) ); - - $this->assertEquals( 'custom-src', $asset['src'] ); - - } - - /** - * Test that adding an asset with an empty suffix will not add the default suffix. - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_no_suffix() { - - add_filter( 'llms_get_script_asset_before_prep', function( $asset, $handle ) { - - if ( 'mock-style-no-suffix' === $handle ) { - $asset = array( - 'file_slug' => 'mock', - 'suffix' => '', - ); - } - - return $asset; - - }, 10, 2 ); - - $asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'mock-style-no-suffix' ) ); - - $this->assertEquals( '', $asset['suffix'] ); - - - - } - - /** - * Test get_scripts() - * - * @since 4.4.0 - * @since 5.5.0 Add `asset_file`. - * - * @return void - */ - public function test_get_defaults_for_scripts() { - - $expect = array( - 'base_file' => LLMS_PLUGIN_FILE, - 'base_url' => LLMS_PLUGIN_URL, - 'suffix' => LLMS_ASSETS_SUFFIX, - 'dependencies' => array(), - 'version' => llms()->version, - 'extension' => '.js', - 'in_footer' => true, - 'path' => 'assets/js', - 'translate' => false, - 'asset_file' => false, - ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_defaults', array( 'script' ) ) ); - - } - - /** - * Test get_styles() - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_defaults_for_styles() { - - $expect = array( - 'base_file' => LLMS_PLUGIN_FILE, - 'base_url' => LLMS_PLUGIN_URL, - 'suffix' => LLMS_ASSETS_SUFFIX, - 'dependencies' => array(), - 'version' => llms()->version, - 'extension' => '.css', - 'media' => 'all', - 'path' => 'assets/css', - 'rtl' => true, - ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_defaults', array( 'style' ) ) ); - - } - - /** - * Test get_definitions() - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_definitions() { - - // Definitions returned. - $this->assertFalse( empty( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'script' ) ) ) ); - $this->assertFalse( empty( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'style' ) ) ) ); - - // Not a real asset type. - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'fake' ) ) ); - - } - - /** - * Test get_definitions_inline() - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_definitions_inline() { - - // No assets. - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'footer' ) ) ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'style' ) ) ); - - // Fake. - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'fake' ) ) ); - - $this->main->enqueue_inline( 'in-header', '', 'header' ); - $this->main->enqueue_inline( 'in-footer', '', 'footer' ); - $this->main->enqueue_inline( 'in-style', '', 'style' ); - - // Reduces to scripts by location. - $this->assertEquals( array( 'in-header'), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) ) ); - $this->assertEquals( array( 'in-footer' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'footer' ) ) ) ); - $this->assertEquals( array( 'in-style' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'style' ) ) ) ); - - $this->main->enqueue_inline( 'in-header-first', '', 'header', 5 ); - - // Sorted by priority. - $this->assertEquals( array( 'in-header-first', 'in-header' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) ) ); - - } - - /** - * Test get_inline_priority() - * - * @since 4.4.0 - * - * @return void - */ - public function test_get_inline_priority() { - - $existing_priorties = array(); - - $i = (float) 5; - while ( $i <= 5.05 ) { - - $this->assertEquals( $i, LLMS_Unit_Test_Util::call_method( $this->main, 'get_inline_priority', array( 5, $existing_priorties ) ) ); - - $existing_priorties[] = array( 'priority' => $i ); - $i += .01; - - } - - } - - /** - * Test is_inline_enqueued() - * - * @since 4.4.0 - * - * @return void - */ - public function test_is_inline_enqueued() { - - // Not enqueued. - $this->assertFalse( $this->main->is_inline_enqueued( 'is-inline-enqueued' ) ); - - // Enqueue. - $this->main->enqueue_inline( 'is-inline-enqueued', 'console.log( 1 );', 'footer' ); - - // Is enqueued. - $this->assertTrue( $this->main->is_inline_enqueued( 'is-inline-enqueued' ) ); - - } - - /** - * Test merge_asset_file() when `asset_file` is `false`. - * - * @since 5.5.0 - * - * @return void - */ - public function test_merge_asset_file_disabled() { - - $asset = array( - 'base_file' => 'fake.php', - 'asset_file' => false, - 'dependencies' => array(), - ); - - $this->assertEquals( $asset, LLMS_Unit_Test_Util::call_method( $this->main, 'merge_asset_file', array( $asset ) ) ); - - } - - /** - * Test output_inline() - * - * @since 4.4.0 - * - * @return void - */ - public function test_output_inline() { - - add_filter( 'llms_assets_debug', '__return_false' ); - $this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - $this->main->enqueue_inline( 'in-header', 'console.log(1);', 'header' ); - $this->main->enqueue_inline( 'in-header-2', 'console.log(2);', 'header' ); - $this->main->enqueue_inline( 'in-footer', 'console.log(1);', 'footer' ); - $this->main->enqueue_inline( 'in-footer-2', 'console.log(2);', 'footer' ); - $this->main->enqueue_inline( 'in-style', 'body{background:red;}', 'style' ); - $this->main->enqueue_inline( 'in-style-2', 'body{color:black;}', 'style' ); - - $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' ) ); - $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' ) ); - - $this->assertOutputEquals( '<style id="llms-inline-styles" type="text/css">body{background:red;}body{color:black;}</style>', array( $this->main, 'output_inline' ), array( 'style' ) ); - - remove_filter( 'llms_assets_debug', '__return_false' ); - - } - - /** - * Test prepare_inline_asset_for_output(): not in debug mode, scripts & styles work the same. - * - * @since 4.4.0 - * - * @return void - */ - public function test_prepare_inline_asset_for_output() { - - $asset = array( - 'handle' => 'fake-handle', - 'asset' => 'console.log(1);', - ); - - add_filter( 'llms_assets_debug', '__return_false' ); - $this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - $this->assertEquals( $asset['asset'], LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'header' ) ) ); - - remove_filter( 'llms_assets_debug', '__return_false' ); - - } - - /** - * Test prepare_inline_asset_for_output(): for scripts. - * - * @since 4.4.0 - * - * @return void - */ - public function test_prepare_inline_asset_for_output_scripts_debug_on() { - - $asset = array( - 'handle' => 'fake-handle', - 'asset' => 'console.log(1);', - ); - - add_filter( 'llms_assets_debug', '__return_true' ); - $this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - $this->assertEquals( "// fake-handle.\nconsole.log(1);\n", LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'header' ) ) ); - - remove_filter( 'llms_assets_debug', '__return_true' ); - - } - - /** - * Test prepare_inline_asset_for_output(): for styles. - * - * @since 4.4.0 - * - * @return void - */ - public function test_prepare_inline_asset_for_output_styles_debug_on() { - - $asset = array( - 'handle' => 'fake-handle', - 'asset' => 'body{background:red;}', - ); - - add_filter( 'llms_assets_debug', '__return_true' ); - $this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - $this->assertEquals( "/* fake-handle. */\nbody{background:red;}\n", LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'style' ) ) ); - - remove_filter( 'llms_assets_debug', '__return_true' ); - - } - - /** - * Test register_script() for a custom asset (added via a filter) - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_script_custom() { - - add_filter( 'llms_get_script_asset_definitions', function( $defs ) { - $defs['mock-script'] = array( - 'file_slug' => 'mock-script', - ); - return $defs; - } ); - - $this->assertTrue( $this->main->register_script( 'mock-script' ) ); - $this->assertAssetIsRegistered( 'script', 'mock-script' ); - - } - - /** - * Test register_script() for a defined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_script_defined() { - - $this->assertTrue( $this->main->register_script( 'llms' ) ); - $this->assertAssetIsRegistered( 'script', 'llms' ); - - } - - /** - * Test register_script() for a defined asset with defined dependencies. - * - * @since 5.5.0 - * - * @return void - */ - public function test_register_script_defined_with_deps() { - - // Dependency is not registered. - $this->assertAssetNotRegistered( 'script', 'llms' ); - - $this->assertTrue( $this->main->register_script( 'llms-quiz' ) ); - $this->assertAssetIsRegistered( 'script', 'llms-quiz' ); - - // Dependency was automatically registered. - $this->assertAssetIsRegistered( 'script', 'llms' ); - - } - - /** - * Test register_script() for an undefined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_script_undefined() { - - $this->assertFalse( $this->main->register_script( 'fake-script' ) ); - $this->assertAssetNotRegistered( 'script', 'fake-script' ); - - } - - /** - * Test register_script() with translations - * - * @since 5.5.0 - * - * @return void - */ - public function test_register_script_with_translations() { - - // LLMS_PLUGIN_URL gets messed up in the testing environment. - $handler = function( $defaults ) { - $defaults['base_url'] = plugins_url() . '/lifterlms'; - return $defaults; - }; - add_filter( 'llms_get_script_asset_defaults', $handler ); - - - $handle = 'llms-test-messages'; - $file = 'assets/js/llms-test-messages.js'; - $md5 = md5( $file ); - $json = file_get_contents( LLMS_Unit_Test_Files::get_asset_path( sprintf( 'lifterlms-en_US-%s.json', $md5 ) ) ); - - $scripts = array( $handle => array( 'translate' => true ) ); - $this->main->define( 'scripts', $scripts ); - - $dirs = array( - WP_LANG_DIR . '/lifterlms', // "Safe" directory. - WP_LANG_DIR . '/plugins', // Default language directory. - plugin_dir_path( LLMS_PLUGIN_FILE ) . 'languages', // Plugin language directory. - ); - - foreach ( $dirs as $dir ) { - - // Load a language file. - $file = LLMS_Unit_Test_Files::copy_asset( sprintf( 'lifterlms-en_US-%s.json', $md5 ), $dir ); - $this->main->register_script( $handle ); - - // The script's translation path should be the intended directory. - $this->assertEquals( $dir, wp_scripts()->registered[ $handle ]->translations_path, $dir ); - - // If we load the script's textdomain we'll see JSON matching the mock file. - $this->assertEquals( $json, load_script_textdomain( $handle, 'lifterlms', $dir ), $dir ); - - // Clean up. - LLMS_Unit_Test_Files::remove( $file ); - wp_deregister_script( $handle ); - - } - - // No files found. - $this->main->register_script( $handle ); - $this->assertNull( wp_scripts()->registered[ $handle ]->translations_path ); - $this->assertFalse( load_script_textdomain( $handle, 'lifterlms' ) ); - - remove_filter( 'llms_get_script_asset_defaults', $handler ); - - } - - /** - * Test register_style() for a custom asset (added via a filter) - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_style_custom() { - - add_filter( 'llms_get_style_asset_definitions', function( $defs ) { - $defs['mock-style'] = array( - 'file_slug' => 'mock-style', - 'rtl' => false, - ); - return $defs; - } ); - - $this->assertTrue( $this->main->register_style( 'mock-style' ) ); - $this->assertAssetIsRegistered( 'style', 'mock-style' ); - - // No RTL is added. - global $wp_styles; - $this->assertEquals( array(), $wp_styles->registered['mock-style']->extra ); - - } - - - /** - * Test register_style() for a defined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_style_defined() { - - $this->assertTrue( $this->main->register_style( 'lifterlms-styles' ) ); - $this->assertAssetIsRegistered( 'style', 'lifterlms-styles' ); - - // Ensure RTL is added. - global $wp_styles; - $expect = array( - 'rtl' => 'replace', - 'suffix' => LLMS_ASSETS_SUFFIX, - ); - $this->assertEquals( $expect, $wp_styles->registered['lifterlms-styles']->extra ); - - } - - /** - * Test register_style() for an asset with defined dependencies - * - * @since 5.5.0 - * - * @return void - */ - public function test_register_style_with_deps() { - - $this->markTestIncomplete( 'Need to rework this test when a qualifying asset is defined.' ); - - // Deps are not registered. - $deps = array( 'llms-datetimepicker', 'llms-quill-bubble', 'webui-popover' ); - foreach ( $deps as $dep ) { - $this->assertAssetNotRegistered( 'style', $dep ); - } - - $this->assertTrue( $this->main->register_style( 'llms-builder-styles' ) ); - $this->assertAssetIsRegistered( 'style', 'llms-builder-styles' ); - - // Deps are registered. - foreach ( $deps as $dep ) { - $this->assertAssetIsRegistered( 'style', $dep ); - } - - } - - /** - * Test register_style() for an undefined asset. - * - * @since 4.4.0 - * - * @return void - */ - public function test_register_style_undefined() { - - $this->assertFalse( $this->main->register_style( 'fake-style' ) ); - $this->assertAssetNotRegistered( 'style', 'fake-style' ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-block-templates.php b/tests/phpunit/unit-tests/class-llms-test-block-templates.php deleted file mode 100644 index 8f6fd4eece..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-block-templates.php +++ /dev/null @@ -1,457 +0,0 @@ -<?php -/** - * Test LLMS_Block_Templates - * - * @package LifterLMS/Tests - * - * @group block_templates - * - * @since 5.8.0 - * @since 5.9.0 Added more tests. - */ -class LLMS_Test_Block_Templates extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 5.8.0 - * - * @return void - */ - public function set_up() { - - $this->main = LLMS_Block_Templates::instance(); - parent::set_up(); - - } - - /** - * Test __construct(). - * - * @since 5.8.0 - * - * @return void - */ - public function test_constructor() { - - // Reset data. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'block_templates_config', null ); - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) ); - - remove_filter( 'get_block_templates', array( $this->main, 'add_llms_block_templates' ), 10 ); - remove_filter( 'pre_get_block_file_template', array( $this->main, 'maybe_return_blocks_template' ), 10 ); - remove_action( 'admin_enqueue_scripts', array( $this->main, 'localize_blocks' ), 9999 ); - - LLMS_Unit_Test_Util::call_method( $this->main, '__construct' ); - - // Configuration runs. - $this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) ); - - // Hooks added. - $this->assertEquals( 10, has_filter( 'get_block_templates', array( $this->main, 'add_llms_block_templates' ) ) ); - $this->assertEquals( 10, has_filter( 'pre_get_block_file_template', array( $this->main, 'maybe_return_blocks_template' ) ) ); - $this->assertEquals( 9999, has_action( 'admin_enqueue_scripts', array( $this->main, 'localize_blocks' ) ) ); - - } - - /** - * Test configure_block_templates(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_configure_block_templates() { - - // Reset data. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'block_templates_config', null ); - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) ); - - // Run configurator. - $this->main->configure_block_templates(); - - $block_templates_config = array( - llms()->plugin_path() . '/templates/' . $this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME => array( - 'slug_prefix' => $this->main::LLMS_BLOCK_TEMPLATES_PREFIX, - 'namespace' => $this->main::LLMS_BLOCK_TEMPLATES_NAMESPACE, - 'blocks_dir' => $this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME, // Relative to the plugin's templates directory. - 'admin_blocks_l10n' => LLMS_Unit_Test_Util::call_method( $this->main, 'block_editor_l10n' ), - 'template_titles' => LLMS_Unit_Test_Util::call_method( $this->main, 'template_titles' ), - ), - ); - - $this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) ); - $this->assertEquals( - $block_templates_config, - LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) - ); - - // Check that the configuration can be extended through a filter - $additional_configuration = array( - '/some/path/to' => array( - 'slug_prefix' => 'some-slug-prefix_', - 'namespace' => 'some/namespace', - 'blocks_dir' => 'blocks-dir', // Relative to the plugin's templates directory. - 'admin_blocks_l10n' => array( 'string-1' => 'String 1' ), - 'template_titles' => array( 'some-archive' => 'Some Archive title' ), - ), - ); - $add_configuration_cb = function( $config ) use ( $additional_configuration ) { - return array_merge( $config, $additional_configuration ); - }; - add_filter( 'llms_block_templates_config', $add_configuration_cb ); - - // Run configurator again. - $this->main->configure_block_templates(); - remove_filter( 'llms_block_templates_config', $add_configuration_cb ); - - $this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) ); - $this->assertEquals( - array_merge( $block_templates_config, $additional_configuration ), - LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) - ); - - // Run configurator again, without the filter, to reinit the configuration. - $this->main->configure_block_templates(); - $this->assertEquals( - $block_templates_config, - LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) - ); - - } - - /** - * Test generate_template_slug_from_path(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_generate_template_slug_from_path() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - $this->assertNotEmpty( reset( $block_templates_config )['slug_prefix'] ); - - // Expecting the slug to be the template name, without the extension, plus the configured slug prefix. - $this->assertEquals( - reset( $block_templates_config )['slug_prefix'] . 'template', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_slug_from_path', - array( - key( $block_templates_config ) . '/template.html', - ) - ) - ); - - // This util is pretty dumb, it expects the block file extension to be 5 chars, dot included, otherwise... - $this->assertEquals( - reset( $block_templates_config )['slug_prefix'] . 'templat', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_slug_from_path', - array( - key( $block_templates_config ) . '/template.htm', - ) - ) - ); - - $this->assertEquals( - reset( $block_templates_config )['slug_prefix'] . 'template', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_slug_from_path', - array( - key( $block_templates_config ) . '/template12345', - ) - ) - ); - - // If you pass a path which is not in the configuration I expect an empty slug. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_slug_from_path', - array( - '/whateverpath/template.html', - ) - ) - ); - - } - - /** - * Test generate_template_namespace_from_path(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_generate_template_namespace_from_path() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - $this->assertNotEmpty( reset( $block_templates_config )['namespace'] ); - - // Expecting the namespace to be the class constant LLMS_BLOCK_TEMPLATES_NAMESPACE. - $this->assertEquals( - $this->main::LLMS_BLOCK_TEMPLATES_NAMESPACE, - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_namespace_from_path', - array( - key( $block_templates_config ) . '/template.html', - ) - ) - ); - - // If you pass a path which is not in the configuration I expect an empty namespace. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_namespace_from_path', - array( - '/whateverpath/template.html', - ) - ) - ); - - } - - /** - * Test generate_template_prefix_from_path(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_generate_template_prefix_from_path() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - $this->assertNotEmpty( reset( $block_templates_config )['slug_prefix'] ); - - // Expecting the prefix to be the class constant LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME. - $this->assertEquals( - $this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME , - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_blocks_dir_from_path', - array( - key( $block_templates_config ) . '/template.html', - ) - ) - ); - - // If you pass a path which is not in the configuration I expect an empty blocks directory. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_blocks_dir_from_path', - array( - '/whateverpath/template.html', - ) - ) - ); - - } - - /** - * Test generate_template_prefix_from_path(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_generate_blocks_dir_from_path() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - $this->assertNotEmpty( reset( $block_templates_config )['blocks_dir'] ); - - // Expecting the prefix to be the class constant LLMS_BLOCK_TEMPLATES_PREFIX - $this->assertEquals( - $this->main::LLMS_BLOCK_TEMPLATES_PREFIX , - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_prefix_from_path', - array( - key( $block_templates_config ) . '/template.html', - ) - ) - ); - - // If you pass a path which is not in the configuration I expect an empty prefix. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'generate_template_prefix_from_path', - array( - '/whateverpath/template.html', - ) - ) - ); - - } - - /** - * Test block_template_config_property_from_path(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_block_template_config_property_from_path() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - - // Non-existent property for an existent path => empty string. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'block_template_config_property_from_path', - array( - key( $block_templates_config ) . '/some/block/template.html', - 'this-property-does-not-exist' - ) - ) - ); - - // Non-existent property for a non-existent path => empty string. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'block_template_config_property_from_path', - array( - '/some/block/template.html', - 'this-property-does-not-exist' - ) - ) - ); - - // Existent property for non-existent path => empty string. - $this->assertEquals( - '', - LLMS_Unit_Test_Util::call_method( - $this->main, - 'block_template_config_property_from_path', - array( - '/some/block/template.html', - 'slug_prefix' - ) - ) - ); - - // Existent property for existent path => property value. - $this->assertEquals( - reset( $block_templates_config )['slug_prefix'], - LLMS_Unit_Test_Util::call_method( - $this->main, - 'block_template_config_property_from_path', - array( - key( $block_templates_config ) . '/some/block/template.html', - 'slug_prefix' - ) - ) - ); - - } - - /** - * Test convert_slug_to_title(). - * - * @since 5.9.0 - * - * @return string Human friendly title converted from the slug. - */ - public function test_convert_slug_to_title() { - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - - // Existent slugs. - $titles = reset( $block_templates_config )['template_titles']; - foreach ( $titles as $slug => $title ) { - $this->assertEquals( - $title, - LLMS_Unit_Test_Util::call_method( - $this->main, - 'convert_slug_to_title', - array( - $slug, - ) - ), - $slug - ); - } - - // Non-existent slug. - $this->assertEquals( - "This Slug Does Not Exist", - LLMS_Unit_Test_Util::call_method( - $this->main, - 'convert_slug_to_title', - array( - 'this-slug-does-not-exist', - ) - ), - $slug - ); - - } - - /** - * Test localize_blocks(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_localize_blocks() { - global $wp_scripts; - - $block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ); - $this->assertNotNull( $block_templates_config ); - - // Add a fake llms-blocks-editor script. - $wp_scripts->registered['llms-blocks-editor'] = new _WP_Dependency( - 'llms-blocks-editor', - '/fake/', - array(), - 'ver', - array() - ); - - // Check localization went through. - $this->assertTrue( $this->main->localize_blocks() ); - - // Check localization is what we expect. - $this->assertEquals( - sprintf( - 'var %1$s = %2$s;', - 'llmsBlockTemplatesL10n', - wp_json_encode( - array_map( - function( $value ) { - return html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' ); - }, - array_merge( ...array_column( $block_templates_config, 'admin_blocks_l10n' ) ) - ) - ) - ), - - $wp_scripts->get_data( 'llms-blocks-editor', 'data' ), - - ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-blocks.php b/tests/phpunit/unit-tests/class-llms-test-blocks.php deleted file mode 100644 index d77f1a188c..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-blocks.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Test inclusion and initialization of the blocks library. - * - * @package LifterLMS/Tests - * - * @group blocks - * @group packages - * - * @since 3.36.3 - * @version 3.36.3 - */ -class LLMS_Test_Blocks extends LLMS_Unit_Test_Case { - - /** - * Test blocks lib exists and is loaded. - * - * @since 3.36.3 - * - * @return void - */ - public function test_blocks_lib_exists() { - $this->assertTrue( class_exists( 'LLMS_Blocks' ) ); - $this->assertTrue( defined( 'LLMS_BLOCKS_VERSION' ) ); - $this->assertNotNull( LLMS_BLOCKS_VERSION ); - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-cache-helper.php b/tests/phpunit/unit-tests/class-llms-test-cache-helper.php deleted file mode 100644 index 42afd8dac8..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-cache-helper.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * Test LLMS_Cache_Helper - * - * @package LifterLMS/Tests - * - * @group cache - * @group cache_helper - * - * @since 4.0.0 - * @version 4.0.0 - */ -class LLMS_Test_Cache_Helper extends LLMS_Unit_Test_Case { - - /** - * Test get_prefix() method. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_prefix() { - - $group = 'mock_prefix'; - - // Cache miss. - wp_cache_delete( 'llms_mock_cache_prefix', $group ); - - $prefix = LLMS_Cache_Helper::get_prefix( $group ); - - // Looks right. - $this->assertEquals( 1, preg_match( '/llms_cache_0.[0-9]{8} [0-9]{10}_/', $prefix ) ); - - // Cache hit. - $this->assertEquals( $prefix, LLMS_Cache_Helper::get_prefix( $group ) ); - - } - - /** - * Test invalidate_group() method. - * - * @since 4.0.0 - * - * @return void - */ - public function test_invalidate_group() { - - $group = 'mock_invalidate'; - - $prefix = LLMS_Cache_Helper::get_prefix( $group ); - - // Cache an item with the prefix. - wp_cache_set( sprintf( 'fake_%s', $prefix ), 'mock_val', $group ); - - $prefix = LLMS_Cache_Helper::invalidate_group( $group ); - - // New prefix should not match the original prefix. - $this->assertNotEquals( $prefix, LLMS_Cache_Helper::get_prefix( $group ) ); - - // Cached item is gone. - $this->assertFalse( wp_cache_get( sprintf( 'fake_%s', $prefix ), $group ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-certificates.php b/tests/phpunit/unit-tests/class-llms-test-certificates.php deleted file mode 100644 index d0791e458e..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-certificates.php +++ /dev/null @@ -1,348 +0,0 @@ -<?php -/** - * Test LLMS_Certificates - * - * @package LifterLMS/Tests - * - * @group certificates - * - * @since 3.37.3 - * @since 4.21.0 Added tests on modify_dom_links() and modify_dom_images(). - * @version 4.21.0 - */ -class LLMS_Test_Certificates extends LLMS_UnitTestCase { - - /** - * Test trigger_engagement() method. - * - * @since 3.37.3 - * @since 3.37.4 Use `$this->create_certificate_template()` from test case base. - * - * @return void - */ - public function test_trigger_engagement() { - - $user = $this->factory->user->create(); - $template = $this->create_certificate_template(); - $related = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $earned = $this->earn_certificate( $user, $template, $related ); - - // User ID. - $this->assertEquals( $user, $earned[0] ); - - // Related ID. - $this->assertEquals( $related, $earned[2] ); - - } - - /** - * Retrieve a certificate export, bypassing the cache. - * - * @since 3.37.3 - * @since 3.37.4 Use `$this->create_certificate_template()` from test case base. - * - * @return void - */ - public function test_get_export_no_cache() { - - $user = $this->factory->user->create(); - $template = $this->create_certificate_template(); - $related = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $earned = $this->earn_certificate( $user, $template, $related ); - - $cert_id = $earned[1]; - - $path = LLMS()->certificates()->get_export( $cert_id ); - $this->assertTrue( false !== strpos( $path, '/uploads/llms-tmp/certificate-mock-certificate-title' ) ); - $this->assertTrue( false !== strpos( $path, '.html' ) ); - - } - - /** - * Retrieve a certificate export using caching. - * - * @since 3.37.3 - * @since 3.37.4 Use `$this->create_certificate_template()` from test case base. - * - * @return void - */ - public function test_get_export_with_cache() { - - $user = $this->factory->user->create(); - $template = $this->create_certificate_template(); - $related = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $earned = $this->earn_certificate( $user, $template, $related ); - - $cert_id = $earned[1]; - - // Generate a new cert when item not found in the cache. - $orig_path = LLMS()->certificates()->get_export( $cert_id, true ); - $this->assertTrue( false !== strpos( $orig_path, '/uploads/llms-tmp/certificate-mock-certificate-title' ) ); - - // Store the filepath for future use. - $this->assertEquals( $orig_path, get_post_meta( $cert_id, '_llms_export_filepath', true ) ); - - // Get it again, should return the original path from the cache. - $cached_path = LLMS()->certificates()->get_export( $cert_id, true ); - $this->assertEquals( $orig_path, $cached_path ); - - // Delete the file (simulate LLMS_TMP_DIR file expiration). - unlink( $orig_path ); - - // Should regen since the file saved in meta data doesn't exist anymore. - $new_path = LLMS()->certificates()->get_export( $cert_id, true ); - $this->assertTrue( $orig_path !== $new_path ); - - } - - /** - * Test modify_dom_links() - * - * @since 4.21.0 - * - * @return void - */ - public function test_modify_dom_links() { - - // Copy test CSSs to the local website for testing purpose. - LLMS_Unit_Test_Files::copy_asset( 'example-style-1.css', WP_CONTENT_DIR ); - LLMS_Unit_Test_Files::copy_asset( 'example-style-2.css', WP_CONTENT_DIR ); - - $stylesheet_hrefs = array( - get_site_url() . '/wp-content/example-style-1.css' => true, // Local. - get_home_url() . '/wp-content/example-style-2.css' => true, // Local. - 'https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&subset=latin,latin-ext&display=swap' => false, // Blocked host. - 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/example-style.css' => true, - 'https://unreacha.ble/style.css' => false, - ); - - $dom = $this->_get_certificate_dom( - array( - 'head' => array_reduce( - array_keys( $stylesheet_hrefs ), - function( $carry, $stylesheet_href ) { - return sprintf( - '%1$s<link rel="stylesheet" href="%2$s" type="test/css" media="all">', - $carry, - $stylesheet_href - ); - } - ) - ) - ); - - LLMS_Unit_Test_Util::call_method( - LLMS()->certificates(), - 'modify_dom_links', - array( $dom ) - ); - - $dom->saveHTML(); - - // Test there are no survived link tags (stylesheets are inlined). - $this->assertEmpty( $dom->getElementsByTagName( 'link' )->length ); - - $head = $dom->getElementsByTagName( 'head' )->item(0)->nodeValue; - - foreach ( $stylesheet_hrefs as $stylesheet_href => $contained ) { - - $stylesheet_raw = LLMS_Unit_Test_Util::call_method( - LLMS()->certificates(), - 'get_stylesheet_raw', - array( $stylesheet_href, false ) - ); - - if ( ! $stylesheet_raw ) { - $this->assertFalse( $contained, $stylesheet_href ); - continue; - } - - if ( $contained ) { - $this->assertStringContainsString( - $stylesheet_raw, - $head, - $stylesheet_href - ); - } else { - $this->assertStringNotContainsString( - $stylesheet_raw, - $head, - $stylesheet_href - ); - } - } - - // Delete copied assets. - LLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/example-style-1.css' ); - LLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/example-style-2.css' ); - - } - - - /** - * Test modify_dom_images() - * - * @since 4.21.0 - * - * @return void - */ - public function test_modify_dom_images() { - - // Copy test images to the local website for testing purpose. - LLMS_Unit_Test_Files::copy_asset( 'klim-musalimov-rDMacl1FDjw-unsplash.jpeg', WP_CONTENT_DIR ); - LLMS_Unit_Test_Files::copy_asset( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg', WP_CONTENT_DIR ); - - $image_srcs = array( - get_site_url() . '/wp-content/klim-musalimov-rDMacl1FDjw-unsplash.jpeg' => true, // Local. - get_home_url() . '/wp-content/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' => true, // Local. - 'https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg' => false, // Blocked host. - 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg' => true, - 'https://unreach.able/christian-fregnan-unsplash.jpg' => false, - ); - - $dom = $this->_get_certificate_dom( - array( - 'certificate' => array_reduce( - array_keys( $image_srcs ), - function( $carry, $image_src ) { - return sprintf( - '%1$s<img src="%2$s" loading="lazy" srcset="%2$s 320w" sizes="(max-width: 320px) 280px">', - $carry, - $image_src - ); - } - ) - ) - ); - - // Block wikimedia host. - add_filter( - 'llms_certificate_export_blocked_image_hosts', - function () { - return array( - 'upload.wikimedia.org' - ); - } - ); - - // Re-init certificates to apply the filter above. - LLMS()->certificates()->init(); - - // Modify DOM images. - LLMS_Unit_Test_Util::call_method( - LLMS()->certificates(), - 'modify_dom_images', - array( $dom ) - ); - - $html = $dom->saveHTML(); - - foreach ( $image_srcs as $image_src => $contained ) { - - // Test the image src URLS are removed. - $this->assertStringNotContainsString( - $image_src, - $html, - $image_src - ); - - $image_data_type = LLMS_Unit_Test_Util::call_method( - LLMS()->certificates(), - 'get_image_data_and_type', - array( $image_src, false ) - ); - - if ( empty( $image_data_type['data'] ) || empty( $image_data_type['type'] ) ) { - $this->assertFalse( $contained, $image_src ); - continue; - } - - $image_data = base64_encode( $image_data_type['data'] ); - - if ( $contained ) { - $this->assertStringContainsString( - $image_data, - $html, - $image_src - ); - } else { - $this->assertStringNotContainsString( - $image_data, - $html, - $image_src - ); - } - - } - - // Get images do not have loading, sizes, and srcset attibutes. - foreach ( $dom->getElementsByTagName( 'img' ) as $img ) { - $this->assertEmpty( $img->getAttribute( 'srcset' ) ); - $this->assertEmpty( $img->getAttribute( 'sizes' ) ); - $this->assertEmpty( $img->getAttribute( 'loading' ) ); - } - - // Clean added filters. - remove_all_filters( 'llms_certificate_export_blocked_image_hosts' ); - - // Delete copied images. - LLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/klim-musalimov-rDMacl1FDjw-unsplash.jpeg' ); - LLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' ); - - } - - /** - * Util to build a DOMDocument similar to the scraped certificate - * - * @since 4.21.0 - * - * @param array $dom_sections Sections of the page. - * @return DOMDocument|WP_Error - */ - private function _get_certificate_dom( $dom_sections ) { - $sections = array( - 'head' => '', - 'certificate' => '', - 'footer' => '', - ); - - $sections = wp_parse_args( $dom_sections, $sections ); - - $html = ' - <!DOCTYPE html> -<html lang="en-US"> - <head> - <meta charset="UTF-8"> - ' - . $sections['head'] . - ' - </head> - <body> - <div class="llms-certificate-container" style="width:800px; height:616px;"> - <div id="certificate-243" class="post-243 llms_certificate type-llms_certificate status-publish hentry"> - <div class="llms-summary">' - . $sections['certificate'] . - '</div> - </div> - </div> - <footer>' - . $sections['footer'] . - '</footer> - </body> -</html>'; - - $dom = llms_get_dom_document( $html ); - if ( is_wp_error( $dom ) ) { - return $dom; - } - - // Don't throw or log warnings. - $libxml_state = libxml_use_internal_errors( true ); - - return $dom; - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-cli.php b/tests/phpunit/unit-tests/class-llms-test-cli.php deleted file mode 100644 index 6bd2b3436f..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-cli.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Test inclusion and initialization of the LLMS-CLI library - * - * @package LifterLMS/Tests - * - * @group cli - * @group packages - * - * @since 5.5.0 - * @version 5.5.0 - */ -class LLMS_Test_CLI extends LLMS_Unit_Test_Case { - - /** - * Test rest package exists and is loaded. - * - * @since 5.5.0 - * - * @return void - */ - public function test_cli_package_exists() { - $this->assertTrue( function_exists( 'llms_cli' ) ); - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-comments.php b/tests/phpunit/unit-tests/class-llms-test-comments.php deleted file mode 100644 index dd11f2f832..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-comments.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php -/** - * Test LLMS_Comments - * - * @package LifterLMS/Tests - * - * @group comments - * - * @since 3.37.12 - */ -class LLMS_Test_Comments extends LLMS_Unit_Test_Case { - - /** - * Test wp_count_comments() when passing in a specific post id. - * - * @since 3.37.12 - * - * @return void - */ - public function test_wp_count_comments_specific_post() { - - $expect = array(); - $this->assertEquals( $expect, LLMS_Comments::wp_count_comments( $expect, 123 ) ); - - } - - /** - * Test wp_count_comments() when the transient already exists. - * - * @since 3.37.12 - * - * @return void - */ - public function test_wp_count_comments_transient_exists() { - - $expect = array( 1 ); - set_transient( 'llms_count_comments', $expect, 10 ); - - $this->assertEquals( $expect, LLMS_Comments::wp_count_comments( $expect, 0 ) ); - - } - - /** - * Test wp_count_comments() when a new stats object should be generated - * - * @since 3.37.12 - * - * @return void - */ - public function test_wp_count_comments_new() { - - // Insert 5 regular comments. - $this->factory->comment->create_many( 5 ); - - // Insert 5 other custom comment types (we don't want to mess with other plugins). - $this->factory->comment->create_many( 5, array( 'comment_type' => 'custom_type' ) ); - - // Insert 5 order notes, these will be excluded. - $this->factory->comment->create_many( 5, array( 'comment_type' => 'llms_order_note' ) ); - - $res = LLMS_Comments::wp_count_comments( array(), 0 ); - - // Ensure the function creates the stats object in the correct format. - $keys = array( 'approved', 'moderated', 'spam', 'trash', 'post-trashed', 'total_comments', 'all' ); - $this->assertEqualSets( $keys, array_keys( get_object_vars( $res ) ) ); - - // Order notes should be excluded. - $this->assertEquals( 10, $res->total_comments ); - $this->assertEquals( 10, $res->all ); - $this->assertEquals( 10, $res->approved ); - - // All of these are default 0. - $this->assertEquals( 0, $res->moderated ); - $this->assertEquals( 0, $res->spam ); - $this->assertEquals( 0, $res->trash ); - $this->assertEquals( 0, $res->{'post-trashed'} ); - - } - - /** - * Test wp_count_comments() when another plugin has already created a stats object we want to modify - * - * @since 3.37.12 - * - * @return void - */ - public function test_wp_count_comments_modify_existing() { - - // Insert 5 regular comments. - $this->factory->comment->create_many( 5 ); - - // Insert 5 other custom comment types (we don't want to mess with other plugins). - $this->factory->comment->create_many( 5, array( 'comment_type' => 'custom_type' ) ); - - // Insert 5 order notes, these will be excluded. - $this->factory->comment->create_many( 5, array( 'comment_type' => 'llms_order_note' ) ); - - remove_filter( 'wp_count_comments', array( 'LLMS_Comments', 'wp_count_comments' ), 999 ); - $stats = wp_count_comments(); - add_filter( 'wp_count_comments', array( 'LLMS_Comments', 'wp_count_comments' ), 999, 2 ); - - $res = LLMS_Comments::wp_count_comments( $stats, 0 ); - - // Ensure the function creates the stats object in the correct format. - $keys = array( 'approved', 'moderated', 'spam', 'trash', 'post-trashed', 'total_comments', 'all' ); - $this->assertEqualSets( $keys, array_keys( get_object_vars( $res ) ) ); - - // Order notes should be excluded. - $this->assertEquals( 10, $res->total_comments ); - $this->assertEquals( 10, $res->all ); - $this->assertEquals( 10, $res->approved ); - - // All of these are default 0. - $this->assertEquals( 0, $res->moderated ); - $this->assertEquals( 0, $res->spam ); - $this->assertEquals( 0, $res->trash ); - $this->assertEquals( 0, $res->{'post-trashed'} ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-db-upgrader.php b/tests/phpunit/unit-tests/class-llms-test-db-upgrader.php deleted file mode 100644 index ef06dee3d5..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-db-upgrader.php +++ /dev/null @@ -1,378 +0,0 @@ -<?php -/** - * Test AJAX Handler - * - * @package LifterLMS/Tests - * - * @group upgrader - * - * @since 5.2.0 - */ -class LLMS_Test_DB_Upgrader extends LLMS_UnitTestCase { - - /** - * Test can_auto_update() - * - * @since 5.2.0 - * - * @return void - */ - public function test_can_auto_update() { - - // All manual. - $updates = array( - '1.0.0' => array( - 'type' => 'manual', - ), - '2.0.0' => array( - 'type' => 'manual', - ), - ); - $upgrader = new LLMS_DB_Upgrader( '0.0.1', $updates ); - $this->assertFalse( $upgrader->can_auto_update() ); - - $upgrader = new LLMS_DB_Upgrader( '1.5.0', $updates ); - $this->assertFalse( $upgrader->can_auto_update() ); - - // As one auto but it's still manual. - $updates['2.0.0']['type'] = 'auto'; - $upgrader = new LLMS_DB_Upgrader( '0.1.0', $updates ); - $this->assertFalse( $upgrader->can_auto_update() ); - - // Only auto so it's okay. - $upgrader = new LLMS_DB_Upgrader( '1.9.999', $updates ); - $this->assertTrue( $upgrader->can_auto_update() ); - - } - - /** - * Test constructor and get_updates() - * - * @since 5.2.0 - * - * @return void - */ - public function test_constructor_and_get_updates() { - - // No update schema passed, use the core included file. - $upgrader = new LLMS_DB_Upgrader( '1.2.3' ); - - $expect = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-db-updates.php'; - $this->assertEquals( $expect, $upgrader->get_updates() ); - - // Pass in a schema. - $schema = array( - '1.2.3' => array( - 'type' => 'manual', - 'updates' => array( - 'fake_callback', - 'fake_callback_2', - ), - ), - '2.0.0' => array( - 'type' => 'auto', - 'updates' => array( - 'fake_callback', - ), - ), - ); - - $upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema ); - $this->assertEquals( $schema, $upgrader->get_updates() ); - - } - - /** - * Test get_callback_prefix() - * - * @since 5.6.0 - * - * @return void - */ - public function test_get_callback_prefix() { - - $upgrader = new LLMS_DB_Upgrader( '1.2.3' ); - - $tests = array( - array( false, '5.0.0', '', ), - array( null, '5.0.0', '', ), - array( true, '5.0.0', 'LLMS\Updates\Version_5_0_0\\', ), - array( true, '5.0.0-beta.1', 'LLMS\Updates\Version_5_0_0\\', ), - array( true, '5.0.0-alpha.1', 'LLMS\Updates\Version_5_0_0\\', ), - array( 'Custom\String\Provided', '1.0.0', 'Custom\String\Provided\Version_1_0_0\\', ), - ); - - foreach ( $tests as $test ) { - - list( $namespace, $version, $expected ) = $test; - - $info = compact( 'namespace' ); - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $upgrader, 'get_callback_prefix', array( $info, $version ) ) ); - - } - - // When `$namespace` not provided in the $info object. - $this->assertEquals( '', LLMS_Unit_Test_Util::call_method( $upgrader, 'get_callback_prefix', array( array(), $version ) ) ); - - } - - /** - * Test enuqeue_updates() when auto updating - * - * @since 5.2.0 - * - * @return void - */ - public function test_enqueue_updates_auto() { - - $schema = array( - '1.5.0' => array( - 'type' => 'auto', - 'updates' => array( - 'update_auto', - ), - ), - ); - - $upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema ); - $upgrader->enqueue_updates(); - - $updater = LLMS_Unit_Test_Util::get_private_property_value( $upgrader, 'updater' ); - $batch = LLMS_Unit_Test_Util::call_method( $updater, 'get_batch' )->data; - - $this->assertEquals( array( 'update_auto' ), $batch ); - - // Reinit the updater for future tests. - LLMS_Install::init_background_updater(); - - } - - /** - * Test enuqeue_updates() when manual updating is required - * - * @since 5.2.0 - * @since 5.6.0 Add tests for automatic namespacing. - * - * @return void - */ - public function test_enqueue_updates_manual() { - - $schema = array( - '1.5.0' => array( - 'type' => 'manual', - 'updates' => array( - 'update_150_1', - 'update_150_2', - ), - ), - '2.0.0' => array( - 'type' => 'auto', - 'updates' => array( - 'update_200', - ), - ), - '3.5.1' => array( - 'type' => 'manual', - 'namespace' => true, - 'updates' => array( - 'update_something', - ), - ), - '3.9.9' => array( - 'type' => 'manual', - 'namespace' => 'Custom\Namespace', - 'updates' => array( - 'update_something', - ), - ), - ); - - $upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema ); - - $upgrader->enqueue_updates(); - - // Check logs. - $expected_logs = array( - 'Queuing 1.5.0 - update_150_1', - 'Queuing 1.5.0 - update_150_2', - 'Queuing 2.0.0 - update_200', - 'Queuing 3.5.1 - LLMS\Updates\Version_3_5_1\update_something', - 'Queuing 3.9.9 - Custom\Namespace\Version_3_9_9\update_something', - ); - $this->assertEquals( $expected_logs, $this->logs->get( 'updater' ) ); - - // Callbacks loaded into queue properly. - $expected_batch = array( - 'update_150_1', - 'update_150_2', - 'update_200', - 'LLMS\Updates\Version_3_5_1\update_something', - 'Custom\Namespace\Version_3_9_9\update_something', - ); - - $updater = LLMS_Unit_Test_Util::get_private_property_value( $upgrader, 'updater' ); - $batch = LLMS_Unit_Test_Util::call_method( $updater, 'get_batch' )->data; - - // Show completion message. - $complete = array_pop( $batch ); - $this->assertInstanceOf( 'LLMS_DB_Upgrader', $complete[0] ); - $this->assertEquals( 'show_notice_complete', $complete[1] ); - - // Rest of the callbacks. - $this->assertEquals( $expected_batch, $batch ); - - // Reinit the updater for future tests. - LLMS_Install::init_background_updater(); - - } - - /** - * Test get_required_updates() and has_required_updates() - * - * @since 5.2.0 - * - * @return void - */ - public function test_get_required_updates_and_has_required_updates() { - - // Mock updates. - $updates = array( - '1.2.3' => array(), - '2.0.0' => array(), - '3.0.5' => array(), - '4.5.6' => array(), - ); - - foreach ( array( '0.1.1', '1.0.0', '1.2.2' ) as $version ) { - $upgrader = new LLMS_DB_Upgrader( $version, $updates ); - $this->assertEquals( $updates, $upgrader->get_required_updates() ); - $this->assertTrue( $upgrader->has_required_updates() ); - } - - unset( $updates['1.2.3'] ); - foreach ( array( '1.2.3', '1.5.0', '1.99.999' ) as $version ) { - $upgrader = new LLMS_DB_Upgrader( $version, $updates ); - $this->assertEquals( $updates, $upgrader->get_required_updates() ); - $this->assertTrue( $upgrader->has_required_updates() ); - } - - unset( $updates['2.0.0'] ); - $upgrader = new LLMS_DB_Upgrader( '2.0.0', $updates ); - $this->assertEquals( $updates, $upgrader->get_required_updates() ); - $this->assertTrue( $upgrader->has_required_updates() ); - - unset( $updates['3.0.5'] ); - $upgrader = new LLMS_DB_Upgrader( '4.1.2', $updates ); - $this->assertEquals( $updates, $upgrader->get_required_updates() ); - $this->assertTrue( $upgrader->has_required_updates() ); - - // No updates. - foreach ( array( '4.5.6', '5.0.0', '10.5.9' ) as $version ) { - $upgrader = new LLMS_DB_Upgrader( $version, $updates ); - $this->assertEquals( array(), $upgrader->get_required_updates() ); - $this->assertFalse( $upgrader->has_required_updates() ); - } - - } - - /** - * Test show_notice_complete() - * - * @since 5.2.0 - * - * @return void - */ - public function test_show_notice_complete() { - - LLMS_Admin_Notices::add_notice( 'bg-db-update-started', 'notice' ); - - $upgrader = new LLMS_DB_Upgrader( '1.2.3' ); - LLMS_Unit_Test_Util::call_method( $upgrader, 'show_notice_complete' ); - - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'bg-db-update-started' ) ); - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update-complete' ) ); - - } - - /** - * Test show_notice_pending() - * - * @since 5.2.0 - * - * @return void - */ - public function test_show_notice_pending() { - - $upgrader = new LLMS_DB_Upgrader( '1.2.3' ); - - // Add a fake notice so we can make sure it's deleted. - LLMS_Admin_Notices::add_notice( 'bg-db-update', 'deleted' ); - LLMS_Unit_Test_Util::call_method( $upgrader, 'show_notice_pending' ); - - // Has notice. - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) ); - - } - - /** - * Test update() when no updates are required. - * - * @since 5.2.0 - * - * @return void - */ - public function test_update_no_required() { - - $upgrader = new LLMS_DB_Upgrader( '5.0.0', array( '1.0.0' => array() ) ); - $this->assertFalse( $upgrader->update() ); - - } - - /** - * Test update() when updates are required. - * - * @since 5.2.0 - * - * @return void - */ - public function test_update_required() { - - LLMS_Admin_Notices::delete_notice( 'bg-db-update' ); - - $schema = array( - '1.5.0' => array( - 'type' => 'manual', - 'updates' => array( - 'update_150_1', - 'update_150_2', - ), - ), - '2.0.0' => array( - 'type' => 'auto', - 'updates' => array( - 'update_200', - ), - ), - ); - - // Manual update. - $upgrader = new LLMS_DB_Upgrader( '1.0.0', $schema ); - $this->assertTrue( $upgrader->update() ); - - // Notice displayed. - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) ); - LLMS_Admin_Notices::delete_notice( 'bg-db-update' ); - - - // Auto update. - $upgrader = new LLMS_DB_Upgrader( '1.9.1', $schema ); - $this->assertTrue( $upgrader->update() ); - - // No notice displayed. - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) ); - // Updates queued. - $this->assertEquals( array( 'Queuing 2.0.0 - update_200' ), $this->logs->get( 'updater' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-engagements.php b/tests/phpunit/unit-tests/class-llms-test-engagements.php deleted file mode 100644 index ae606ab811..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-engagements.php +++ /dev/null @@ -1,189 +0,0 @@ -<?php -/** - * Tests for LLMS_Engagements class - * - * @package LifterLMS/Tests - * - * @group engagements - * - * @since 4.4.1 - * @since 4.4.3 Test different emails triggered by the same post are correctly sent. - */ -class LLMS_Test_Engagements extends LLMS_Unit_Test_Case { - - /** - * Setup test case - * - * @since 4.4.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->main = llms()->engagements(); - reset_phpmailer_instance(); - } - - /** - * Teardown test case - * - * @since 4.4.1 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - reset_phpmailer_instance(); - - } - - /** - * Test handle_email() as triggered by a related post type that's enrollable. - * - * @since 4.4.1 - * - * @return void - */ - public function test_handle_email_with_course_posts() { - - $mailer = tests_retrieve_phpmailer_instance(); - - $user = $this->factory->user->create_and_get(); - $email = $this->factory->post->create( array( - 'post_type' => 'llms_email', - 'meta_input' => array( - '_llms_email_subject' => 'Engagement Email', - ), - ) ); - $course = $this->factory->course->create_and_get( array( - 'sections' => 1, - 'lessons' => 1, - 'quizzes' => 0, - ) ); - - // Shouldn't send because of enrollment. - $send = $this->main->handle_email( array( $user->ID, $email, $course->get( 'id' ) ) ); - $this->assertIsWPError( $send ); - $this->assertWPErrorCodeEquals( 'llms_engagement_email_not_sent_enrollment', $send ); - $this->assertFalse( $mailer->get_sent() ); - - llms_enroll_student( $user->ID, $course->get( 'id' ) ); - - // Try from course, section, and lesson. - $send_ids = array( $course->get( 'id' ), $course->get_sections( 'ids' )[0], $course->get_lessons( 'ids' )[0] ); - foreach ( $send_ids as $post_id ) { - - // Send the email. - $this->assertTrue( $this->main->handle_email( array( $user->ID, $email, $post_id ) ) ); - - // Email sent. - $sent = $mailer->get_sent(); - $this->assertEquals( $user->user_email, $sent->to[0][0] ); - $this->assertEquals( 'Engagement Email', $sent->subject ); - - // User meta recorded. - $this->assertEquals( $email, llms_get_user_postmeta( $user->ID, $post_id, '_email_sent' ) ); - - // Reset the mailer. - reset_phpmailer_instance(); - $mailer = tests_retrieve_phpmailer_instance(); - - // Shouldn't send again because of dupcheck. - $send = $this->main->handle_email( array( $user->ID, $email, $post_id ) ); - $this->assertIsWPError( $send ); - $this->assertWPErrorCodeEquals( 'llms_engagement_email_not_sent_dupcheck', $send ); - $this->assertFalse( $mailer->get_sent() ); - - } - - } - - /** - * Test handle_email() as triggered by the same related post type with different emails. - * - * @since 4.4.3 - * - * @return void - */ - public function test_handle_different_emails_same_trigger() { - - $mailer = tests_retrieve_phpmailer_instance(); - - $user = $this->factory->user->create_and_get(); - - $emails = $this->factory->post->create_many( - 2, - array( - 'post_type' => 'llms_email', - 'meta_input' => array( - '_llms_email_subject' => 'Engagement Email', - ), - ) - ); - - $course = $this->factory->course->create( array( - 'sections' => 0, - 'lessons' => 0, - 'quizzes' => 0, - ) ); - - llms_enroll_student( $user->ID, $course ); - - // Send the email. - $this->assertTrue( $this->main->handle_email( array( $user->ID, $emails[0], $course ) ) ); - - // Email sent. - $sent = $mailer->get_sent(); - $this->assertEquals( $user->user_email, $sent->to[0][0] ); - $this->assertEquals( 'Engagement Email', $sent->subject ); - - // User meta recorded. - $this->assertEquals( $emails[0], llms_get_user_postmeta( $user->ID, $course, '_email_sent' ) ); - - // Reset the mailer. - reset_phpmailer_instance(); - $mailer = tests_retrieve_phpmailer_instance(); - - // Should send the new mail. - $this->assertTrue( $this->main->handle_email( array( $user->ID, $emails[1], $course ) ) ); - - // Email sent. - $sent = $mailer->get_sent(); - $this->assertEquals( $user->user_email, $sent->to[0][0] ); - $this->assertEquals( 'Engagement Email', $sent->subject ); - - // User meta recorded. - $this->assertEquals( $emails[1], llms_get_user_postmeta( $user->ID, $course, '_email_sent' ) ); - - } - - /** - * Test handle_email() with no related post (as found during registration) - * - * @since 4.4.1 - * - * @return void - */ - public function test_handle_email_with_registration() { - - $mailer = tests_retrieve_phpmailer_instance(); - - $user = $this->factory->user->create_and_get(); - $email = $this->factory->post->create( array( - 'post_type' => 'llms_email', - 'meta_input' => array( - '_llms_email_subject' => 'Engagement Email', - ), - ) ); - - $this->assertTrue( $this->main->handle_email( array( $user->ID, $email, '' ) ) ); - $sent = $mailer->get_sent(); - $this->assertEquals( $user->user_email, $sent->to[0][0] ); - $this->assertEquals( 'Engagement Email', $sent->subject ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-events-core.php b/tests/phpunit/unit-tests/class-llms-test-events-core.php deleted file mode 100644 index ce3866f549..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-events-core.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Test core events - * - * @package LifterLMS_Tests/Classes - * - * @group events - * @group events_core - * - * @since 3.36.0 - * @version 3.36.0 - */ -class LLMS_Test_Events_Core extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.36.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->events = new LLMS_Events_Core(); - } - - /** - * Test on_signon() method - * - * @since 3.36.0 - * - * @return void - */ - public function test_on_signon() { - - $user = $this->factory->user->create_and_get(); - - $event = $this->events->on_signon( $user->user_login, $user ); - - $this->assertTrue( is_a( $event, 'LLMS_Event' ) ); - $this->assertEquals( $user->ID, $event->get( 'actor_id' ) ); - $this->assertEquals( $user->ID, $event->get( 'object_id' ) ); - - $this->assertEquals( 'user', $event->get( 'object_type' ) ); - $this->assertEquals( 'account', $event->get( 'event_type' ) ); - $this->assertEquals( 'signon', $event->get( 'event_action' ) ); - - } - - /** - * Test on_signout() method - * - * @since 3.36.0 - * @since 4.5.0 Added test on the method returning `false` when no user was logged in. - * - * @return void - */ - public function test_on_signout() { - - // No user logged, no event created. - $this->assertFalse( $this->events->on_signout() ); - - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - $event = $this->events->on_signout(); - - $this->assertTrue( is_a( $event, 'LLMS_Event' ) ); - $this->assertEquals( $user, $event->get( 'actor_id' ) ); - $this->assertEquals( $user, $event->get( 'object_id' ) ); - - $this->assertEquals( 'user', $event->get( 'object_type' ) ); - $this->assertEquals( 'account', $event->get( 'event_type' ) ); - $this->assertEquals( 'signout', $event->get( 'event_action' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-events-query.php b/tests/phpunit/unit-tests/class-llms-test-events-query.php deleted file mode 100644 index d16f87664b..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-events-query.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php -/** - * Test events query - * - * @package LifterLMS/Tests - * - * @group events - * @group query - * @group dbquery - * - * @since 4.7.0 - */ -class LLMS_Test_Events_Query extends LLMS_Unit_Test_Case { - - /** - * Setup the test case - * - * @since 3.36.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - } - - /** - * Teardown the test case - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * @return void - */ - public function tear_down() { - parent::tear_down(); - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events" ); - } - - - /** - * Test that the events query, using default args, calculates found rows - * - * @since 4.7.0 - * - * @return void - */ - public function test_query_with_default_args_calculates_found_rows() { - $query = new LLMS_Events_Query(); - $sql = LLMS_Unit_Test_Util::call_method( $query, 'preprare_query' ); - $this->assertSame( 0, strpos( $sql, 'SELECT SQL_CALC_FOUND_ROWS' ) ); - } - - /** - * Test that the events query, passing no_found_rows as true doesn't calculate found rows - * - * @since 4.7.0 - * - * @return void - */ - public function test_query_correctly_doesnt_calculate_found_rows() { - $query = new LLMS_Events_Query( - array( - 'no_found_rows' => true, - ) - ); - $sql = LLMS_Unit_Test_Util::call_method( $query, 'preprare_query' ); - $this->assertSame( false, strpos( $sql, 'SQL_CALC_FOUND_ROWS' ) ); - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-events.php b/tests/phpunit/unit-tests/class-llms-test-events.php deleted file mode 100644 index cc2b49ea93..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-events.php +++ /dev/null @@ -1,254 +0,0 @@ -<?php -/** - * Test events - * - * @package LifterLMS/Tests - * - * @group events - * - * @since 3.36.0 - * @version 4.5.0 - */ -class LLMS_Test_Events extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.36.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->events = LLMS()->events(); - } - - /** - * Teardown the test case. - * - * @since 3.36.0 - * @since 4.5.0 Truncate open sessions table. - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - parent::tear_down(); - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events" ); - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions" ); - } - - /** - * Test missing fields error when recording - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_missing_fields() { - - $event = array(); - - $ret = $this->events->record( $event ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret ); - $this->assertEquals( 5, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) ); - - $event['actor_id'] = 1; - $ret = $this->events->record( $event ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret ); - $this->assertEquals( 4, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) ); - - $event['object_type'] = 'user'; - $ret = $this->events->record( $event ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret ); - $this->assertEquals( 3, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) ); - - $event['object_id'] = 1; - $ret = $this->events->record( $event ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret ); - $this->assertEquals( 2, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) ); - - $event['event_type'] = 'account'; - $ret = $this->events->record( $event ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret ); - $this->assertEquals( 1, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) ); - - } - - /** - * Test recording an invalid event - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_invalid_event() { - - $args = array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'fake', - 'event_action' => 'mock', - ); - $ret = $this->events->record( $args ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms_event_record_invalid_event', $ret ); - - } - - /** - * Test success recording event - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_success() { - - $args = array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'account', - 'event_action' => 'signon', - ); - $ret = $this->events->record( $args ); - - $this->assertTrue( is_a( $ret, 'LLMS_Event' ) ); - foreach ( $args as $key => $expect ) { - $this->assertEquals( $expect, $ret->get( $key ) ); - } - - } - - /** - * Test success recording event with meta - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_success_with_metas() { - - $args = array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'account', - 'event_action' => 'signon', - 'meta' => array( - 'meta_key' => 'meta_val', - ), - ); - $ret = $this->events->record( $args ); - - $this->assertTrue( is_a( $ret, 'LLMS_Event' ) ); - foreach ( $args as $key => $expect ) { - - if ( 'meta' === $key ) { - $this->assertEquals( $expect, $ret->get_meta() ); - } else { - $this->assertEquals( $expect, $ret->get( $key ) ); - } - - } - - } - - /** - * Test errors when recording many events - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_many_with_errors() { - - // All errors. - $events = array( - array(), - array(), - ); - $ret = $this->events->record_many( $events ); - - $this->assertIsWPError( $ret ); - $errors = $ret->get_error_data( 'llms_events_record_many_errors' ); - $this->assertEquals( 2, count( $errors ) ); - foreach ( $errors as $stat ) { - $this->assertIsWPError( $stat ); - } - - $events = array( - array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'account', - 'event_action' => 'signon', - ), - array(), - ); - - // One error with one success. - $ret = $this->events->record_many( $events ); - $this->assertIsWPError( $ret ); - $errors = $ret->get_error_data( 'llms_events_record_many_errors' ); - $this->assertEquals( 1, count( $errors ) ); - foreach ( $errors as $stat ) { - $this->assertIsWPError( $stat ); - } - - // Query rolled back. - global $wpdb; - $this->assertEquals( 0, $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events" ) ); - - } - - /** - * Test success recording many events - * - * @since 3.36.0 - * - * @return void - */ - public function test_record_many_success() { - - $events = array( - array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'account', - 'event_action' => 'signon', - ), - array( - 'actor_id' => 1, - 'object_type' => 'user', - 'object_id' => 1, - 'event_type' => 'account', - 'event_action' => 'signon', - ), - ); - - $ret = $this->events->record_many( $events ); - - foreach ( $ret as $event ) { - $this->assertTrue( is_a( $event, 'LLMS_Event' ) ); - } - - // Query committed. - global $wpdb; - // 3 = the two events created above plus 1 for the session opened. - $this->assertEquals( 3, $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events" ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-frontend-assets.php b/tests/phpunit/unit-tests/class-llms-test-frontend-assets.php deleted file mode 100644 index 0c2ad186ec..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-frontend-assets.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php -/** - * LLMS Frontend Assets Tests - * - * @package LifterLMS/Tests - * - * @group assets - * @group frontend_assets - * - * @since 4.4.0 - */ -class LLMS_Test_Frontend_Assets extends LLMS_UnitTestCase { - - /** - * Retrieves a list of enqueued inline scripts from the LLMS_Assets instance. - * - * @since 5.6.0 - * - * @return array - */ - private function get_inline_scripts() { - return LLMS_Unit_Test_Util::get_private_property_value( llms()->assets, 'inline' ); - } - - /** - * Test enqueue_content_protection(). - * - * @since 5.6.0 - * - * @return void - */ - public function test_enqueue_content_protection() { - - // Content protection off & user is logged out: no scripts loaded. - update_option( 'lifterlms_content_protection', 'no' ); - LLMS_Frontend_Assets::enqueue_content_protection(); - $this->assertEquals( array(), $this->get_inline_scripts() ); - - // Content protection is on and user is logged out: scripts are loaded. - update_option( 'lifterlms_content_protection', 'yes' ); - LLMS_Frontend_Assets::enqueue_content_protection(); - $this->assertArrayHasKey( 'llms-integrity', $this->get_inline_scripts() ); - - LLMS_Unit_Test_Util::set_private_property( llms()->assets, 'inline', array() ); - - // Admin can bypass restrictions, script is not loaded. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Frontend_Assets::enqueue_content_protection(); - $this->assertEquals( array(), $this->get_inline_scripts() ); - - LLMS_Unit_Test_Util::set_private_property( llms()->assets, 'inline', array() ); - - // Student can't copy content. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'student' ) ) ); - LLMS_Frontend_Assets::enqueue_content_protection(); - $this->assertArrayHasKey( 'llms-integrity', $this->get_inline_scripts() ); - - LLMS_Unit_Test_Util::set_private_property( llms()->assets, 'inline', array() ); - - } - - /** - * Test inline script management functions - * - * @since 3.4.1 - * - * @expectedDeprecated LLMS_Frontend_Assets::enqueue_inline_script() - * @expectedDeprecated LLMS_Frontend_Assets::is_inline_enqueued() - * - * @return void - */ - public function test_inline_scripts() { - - // New script should return true. - $this->assertTrue( LLMS_Frontend_Assets::enqueue_inline_script( 'test-id', 'alert("hello");', 'footer', 25 ) ); - - // Script should be enqueued. - $this->assertTrue( LLMS_Frontend_Assets::is_inline_script_enqueued( 'test-id' ) ); - - // Fake script not enqueued. - $this->assertFalse( LLMS_Frontend_Assets::is_inline_script_enqueued( 'fake-id' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-functions-access.php b/tests/phpunit/unit-tests/class-llms-test-functions-access.php deleted file mode 100644 index d3f09dfdf5..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-functions-access.php +++ /dev/null @@ -1,385 +0,0 @@ -<?php -/** - * Tests for LifterLMS Access Functions. - * - * @group access - * - * @since 3.7.3 - * @since 3.16.0 Unknown. - * @since 3.37.10 Added tests on sitewide membership restriction. - */ -class LLMS_Test_Functions_Access extends LLMS_UnitTestCase { - - /** - * Get a formatted date for setting time period related restrictions. - * - * @param string $offset adjust day via strtotime - * @param string $format desired returned format, passed to date() - * @return string - * @since 3.7.3 - * @version 3.7.3 - */ - private function get_date( $offset = '+7 days', $format = 'm/d/y' ) { - return date( $format, strtotime( $offset, current_time( 'timestamp' ) ) ); - } - - /** - * Test drip restrictions. - * - * @since 3.16.0 - * - * @return void - */ - public function test_llms_is_post_restricted_by_drip_settings() { - - $course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0]; - $course = llms_get_post( $course_id ); - $lesson = $course->get_lessons()[0]; - $lesson_id = $lesson->get( 'id' ); - $student = $this->get_mock_student(); - wp_set_current_user( $student->get_id() ); - $student->enroll( $course_id ); - - // no drip settings, lesson is currently available. - $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - // date in past so the lesson is available. - $lesson = llms_get_post( $lesson_id ); - $lesson->set( 'drip_method', 'date' ); - $lesson->set( 'date_available', '12/12/2012' ); - $lesson->set( 'time_available', '12:12 AM' ); - $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - // date in future so lesson not available. - $lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) ); - $this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - // available 3 days after enrollment. - $lesson->set( 'drip_method', 'enrollment' ); - $lesson->set( 'days_before_available', '3' ); - $this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - // now available. - llms_mock_current_time( '+4 days' ); - $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - llms_reset_current_time(); - $lesson->set( 'drip_method', 'start' ); - $course->set( 'start_date', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) ); - - // not available until 3 days after course start date. - $this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - // now available. - llms_mock_current_time( '+4 days' ); - $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); - - } - - /** - * Test restricted by membership. - * - * @since Unknown. - * - * @return void - */ - public function test_llms_is_post_restricted_by_membership() { - - $memberships = $this->factory->post->create_many( 2, array( - 'post_type' => 'llms_membership', - ) ); - $post_id = $this->factory->post->create(); - $student = $this->get_mock_student(); - $uid = $student->get_id(); - - - $this->assertFalse( llms_is_post_restricted_by_membership( $post_id ) ); - $this->assertFalse( llms_is_post_restricted_by_membership( $post_id, $uid ) ); - - update_post_meta( $post_id, '_llms_restricted_levels', $memberships ); - update_post_meta( $post_id, '_llms_is_restricted', 'yes' ); - - $this->assertEquals( $memberships[0], llms_is_post_restricted_by_membership( $post_id ) ); - $this->assertEquals( $memberships[0], llms_is_post_restricted_by_membership( $post_id, $uid ) ); - - $out = llms_is_post_restricted_by_membership( $post_id ); - $in = llms_is_post_restricted_by_membership( $post_id, $uid ); - - $student->enroll( $memberships[1] ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_membership( $post_id, $uid ) ); - - } - - /** - * Test restriction by membership sitewide. - * - * @since Unknown. - * - * @return void - */ - public function test_llms_is_post_restricted_by_sitewide_membership() { - - $memberships = $this->factory->post->create_many( 2, array( - 'post_type' => 'llms_membership', - ) ); - $post_id = $this->factory->post->create(); - - // create a page where redirect to. - $redirect_id = $this->factory->post->create( array( - 'post_type' => 'page' - ) ); - - /** - * Create pages that must be always accessible. - */ - - // create and set privacy policy page. - update_option( 'wp_page_for_privacy_policy', $this->factory->post->create( array( - 'post_type' => 'page' - ) ) ); - - // create and set terms page. - update_option( 'lifterlms_terms_page_id', $this->factory->post->create( array( - 'post_type' => 'page' - ) ) ); - - // create and set memberships catalog page. - update_option( 'lifterlms_memberships_page_id', $this->factory->post->create( array( - 'post_type' => 'page' - ) ) ); - - // create and set myaccount page. - update_option( 'lifterlms_myaccount_page_id', $this->factory->post->create( array( - 'post_type' => 'page' - ) ) ); - - // create and set checkout page. - update_option( 'lifterlms_checkout_page_id', $this->factory->post->create( array( - 'post_type' => 'page' - ) ) ); - - // require membership sitewide. - update_option( 'lifterlms_membership_required', $memberships[1] ); - - // set membership's restriction redirection. - update_post_meta( $memberships[1], '_llms_redirect_page_id', $redirect_id ); - update_post_meta( $memberships[1], '_llms_restriction_redirect_type', 'page' ); - - // I expect a post or another membership to be restricted. - // While I expect the redirection page and the membership set as requirement to be accessible. - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $redirect_id ) ); - - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) ); - - // unset the redirection page. - // I expect a post, the former redirection page or another membership to be restricted. - // While I expect membership set as requirement to be accessible. - update_post_meta( $memberships[1], '_llms_redirect_page_id', '' ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $redirect_id ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) ); - - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) ); - - // re-set the redirection page, but set the restriction redirect type as 'custom'. - // I expect a post, the former redirection page or another membership to be restricted. - // While I expect membership set as requirement to be accessible. - update_post_meta( $memberships[1], '_llms_redirect_page_id', $redirect_id ); - update_post_meta( $memberships[1], '_llms_restriction_redirect_type', 'custom' ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) ); - $this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $redirect_id ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) ); - - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) ); - - // unset the membership enrollment requirement. - // I expect 'everything' to be not restricted. - update_option( 'lifterlms_membership_required', '' ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $post_id ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $redirect_id ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) ); - $this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) ); - } - - /** - * Test the llms_is_post_restricted_by_prerequisite() function. - * - * @since 3.8.0 - * - * @return void - */ - public function test_llms_is_post_restricted_by_prerequisite() { - - $courses = $this->generate_mock_courses( 3, 2, 1, 1 ); - - $prereq_course_id = $courses[0]; - - $course_id = $courses[1]; - $course = llms_get_post( $course_id ); - - $track = wp_insert_term( 'mock track', 'course_track' ); - $track_id = $track['term_id']; - $course_in_track_id = $courses[2]; - wp_set_post_terms( $course_in_track_id, $track_id, 'course_track' ); - - $lessons = $course->get_lessons( 'ids' ); - - $lesson_2 = llms_get_post( $lessons[1] ); - $lesson_2->set( 'has_prerequisite', 'yes' ); - $lesson_2->set( 'prerequisite', $lessons[0] ); - - $test_ids = array_merge( $lessons, $course->get_quizzes() ); - - $this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id ); - - $student_id = $this->factory->user->create( array( 'role' => 'student' ) ); - - // results should all be the same with the student b/c nothing completed. - $this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $student_id ); - - // results differ once student completes courses. - $this->complete_courses_for_student( $student_id, $courses ); - - $this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $student_id ); - - } - - /** - * test_llms_is_post_restricted_by_prerequisite() runs this series of assertions several times. - * - * @since 3.7.3 - * @since 3.12.0 Unknown. - * @since 4.9.0 Remove default value of `$test_ids` parameter for php8 compatibility. - * - * @param array $test_ids Array of post ids to test the llms_is_post_restricted_by_prerequisite() against. - * @param obj $course Course object. - * @param int $prereq_course_id Post id of the prereq course. - * @param int $track_id Term id of the prereq track. - * @param int $user_id Wp user id of a student. - * @return void - */ - private function prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $user_id = null ) { - - $student = $user_id ? new LLMS_Student( $user_id ) : null; - - foreach ( $test_ids as $test_id ) { - - $course->set( 'has_prerequisite', 'no' ); - $course->set( 'prerequisite', '' ); - $course->set( 'prerequisite_track', '' ); - - $post = llms_get_post( $test_id ); - - if ( 'lesson' === get_post_type( $test_id ) && $post->has_prerequisite() ) { - - $lesson_prereq_id = $post->get( 'prerequisite' ); - $lesson_res = $student && $student->is_complete( $lesson_prereq_id, 'lesson' ) ? false : array( - 'type' => 'lesson', - 'id' => $lesson_prereq_id, - ); - $this->assertEquals( $lesson_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) ); - - } - - // set a course prereq. - $course->set( 'has_prerequisite', 'yes' ); - $course->set( 'prerequisite', $prereq_course_id ); - $prereq_course_res = $student && $student->is_complete( $prereq_course_id, 'course' ) ? false : array( - 'type' => 'course', - 'id' => $prereq_course_id, - ); - $this->assertEquals( $prereq_course_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) ); - - // set a track prereq. - $course->set( 'prerequisite_track', $track_id ); - - // checks course prereq first and only returns one. - $this->assertEquals( $prereq_course_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) ); - - // no course prereq, returns track id. - $course->set( 'prerequisite', '' ); - $prereq_track_res = $student && $student->is_complete( $track_id, 'course_track' ) ? false : array( - 'type' => 'course_track', - 'id' => $track_id, - ); - $this->assertEquals( $prereq_track_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) ); - - } - - } - - /** - * Test the llms_is_post_restricted_by_time_period() function. - * - * @since 3.7.3 - * - * @return void - */ - public function test_llms_is_post_restricted_by_time_period() { - - $courses = $this->generate_mock_courses( 1, 1, 1, 1 ); - $course_id = $courses[0]; - $course = llms_get_post( $course_id ); - - $test_ids = array_merge( array( $course_id ), $course->get_lessons( 'ids' ), $course->get_quizzes() ); - - foreach ( $test_ids as $test_post_id ) { - - $course->set( 'time_period', 'no' ); - - // no time period. - $this->assertFalse( llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // enable the restriction. - $course->set( 'time_period', 'yes' ); - - // no dates set the course is closed without dates. - $this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // start date in the future. - $course->set( 'start_date', $this->get_date( '+7 days' ) ); - $this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // start date in past. - $course->set( 'start_date', $this->get_date( '-7 days' ) ); - $this->assertFalse( llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // start date in past and end date in past. - $course->set( 'end_date', $this->get_date( '-5 days' ) ); - $this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // no start date, end date in past. - $course->set( 'start_date', '' ); - $this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) ); - - // no start date end in future. - $course->set( 'end_date', $this->get_date( '+7 days' ) ); - $this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-functions-privacy.php b/tests/phpunit/unit-tests/class-llms-test-functions-privacy.php deleted file mode 100644 index 37ff4ebe9f..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-functions-privacy.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php -/** - * Tests for LifterLMS Privacy Functions - * @group functions - * @group functions_privacy - * @group privacy - * @since 3.19.0 - * @version 3.19.0 - */ -class LLMS_Test_Functions_Privacy extends LLMS_UnitTestCase { - - /** - * Test llms_are_terms_and_conditions_required() - * @return void - * @since 3.3.1 - * @version 3.3.1 - */ - public function test_llms_are_terms_and_conditions_required() { - - // terms true & page id numeric - update_option( 'lifterlms_registration_require_agree_to_terms', 'yes' ); - update_option( 'lifterlms_terms_page_id', '1' ); - $this->assertTrue( llms_are_terms_and_conditions_required() ); - - // terms true & page id non-numeric - update_option( 'lifterlms_registration_require_agree_to_terms', 'yes' ); - update_option( 'lifterlms_terms_page_id', 'brick' ); - $this->assertFalse( llms_are_terms_and_conditions_required() ); - - // terms true & no page id - update_option( 'lifterlms_terms_page_id', '' ); - $this->assertFalse( llms_are_terms_and_conditions_required() ); - - // terms true & page id 0 - update_option( 'lifterlms_terms_page_id', '0' ); - $this->assertFalse( llms_are_terms_and_conditions_required() ); - - // terms false and page id good - update_option( 'lifterlms_registration_require_agree_to_terms', 'no' ); - update_option( 'lifterlms_terms_page_id', '1' ); - $this->assertFalse( llms_are_terms_and_conditions_required() ); - - update_option( 'lifterlms_registration_require_agree_to_terms', 'no' ); - update_option( 'lifterlms_terms_page_id', 'brick' ); - $this->assertFalse( llms_are_terms_and_conditions_required() ); - - } - - function test_llms_get_privacy_notice() { - - $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() ); - - update_option( 'llms_privacy_notice', 'The {{policy}} says things' ); - - $this->assertEquals( 'The {{policy}} says things', llms_get_privacy_notice() ); - - // empty b/c no page set - $this->assertEmpty( llms_get_terms_notice( true ) ); - - // set a page - $page_id = $this->factory->post->create( array( - 'post_title' => 'The Page Title', - 'post_type' => 'page', - ) ); - update_option( 'wp_page_for_privacy_policy', $page_id ); - - // merging works - $this->assertEquals( 'The ' . llms_get_option_page_anchor( 'wp_page_for_privacy_policy' ) . ' says things', llms_get_privacy_notice( true ) ); - - // empty the option - update_option( 'llms_privacy_notice', '' ); - - // empty b/c there's no option anymore - $this->assertEmpty( llms_get_terms_notice( true ) ); - - } - - /** - * test llms_get_terms_notice() - * @return void - * @since 3.19.0 - * @version 3.19.0 - */ - function test_llms_get_terms_notice() { - - // default - $this->assertEquals( 'I have read and agree to the {{terms}}.', llms_get_terms_notice() ); - - update_option( 'llms_terms_notice', 'I agree to {{terms}}' ); - - $this->assertEquals( 'I agree to {{terms}}', llms_get_terms_notice() ); - - // returns empty string when no page set - $this->assertEmpty( llms_get_terms_notice( true ) ); - - // set the page - $page_id = $this->factory->post->create( array( - 'post_title' => 'The Page Title', - 'post_type' => 'page', - ) ); - update_option( 'lifterlms_terms_page_id', $page_id ); - - // test the merged get - $this->assertEquals( 'I agree to ' . llms_get_option_page_anchor( 'lifterlms_terms_page_id' ), llms_get_terms_notice( true ) ); - - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-functions-quiz.php b/tests/phpunit/unit-tests/class-llms-test-functions-quiz.php deleted file mode 100644 index df73d21bed..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-functions-quiz.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php -/** - * Tests for LifterLMS Core Functions - * @group functions - * @group functions_quiz - * @group quizzes - * @group quiz - * @since 3.16.0 - * @version 3.16.12 - */ -class LLMS_Test_Functions_Quiz extends LLMS_UnitTestCase { - - /** - * Test picture choice columns - * @return void - * @since 3.16.0 - * @version 3.16.0 - */ - public function test_llms_get_picture_choice_question_cols() { - - $combos = array( - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - 5 => 3, - 6 => 3, - 7 => 4, - 8 => 4, - 9 => 3, - 10 => 5, - 11 => 4, - 12 => 4, - 13 => 5, - 14 => 5, - 15 => 5, - 16 => 4, - 17 => 3, - 18 => 3, - 19 => 5, - 20 => 5, - 21 => 3, - 22 => 4, - 23 => 4, - 24 => 4, - 25 => 5, - 26 => 5, - 27 => 5, - 45 => 5, - 999 => 5, - 9999 => 5, - ); - - foreach ( $combos as $choices => $expected_cols ) { - - $this->assertEquals( $expected_cols, llms_get_picture_choice_question_cols( $choices ) ); - - } - - } - - /** - * Test llms_shuffle_choices - * @return void - * @since 3.16.12 - * @version 3.16.12 - */ - public function test_llms_shuffle_choices() { - - // 0 & 1 elements can't really be shuffled... - $choices = array(); - $this->assertEquals( $choices, llms_shuffle_choices( $choices ) ); - - $choices = array( 1 ); - $this->assertEquals( $choices, llms_shuffle_choices( $choices ) ); - - // 2 or more items will never match the original after shuffling - $i = 2; - while( $i <= 26 ) { - - $choices = range( 0, $i ); - $this->assertNotEquals( $choices, llms_shuffle_choices( $choices ) ); - $i++; - - } - - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php b/tests/phpunit/unit-tests/class-llms-test-generator-courses.php deleted file mode 100644 index 4ab578f171..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php +++ /dev/null @@ -1,715 +0,0 @@ -<?php -/** - * LLMS_Generator_Courses Tests - * - * @package LifterLMS/Tests - * - * @group generator - * @group generator_courses - * - * @since 4.7.0 - */ -class LLMS_Test_Generator_Courses extends LLMS_UnitTestCase { - - /** - * Load required class - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/class-llms-generator-courses.php'; - - } - - /** - * Setup the test case - * - * @since 4.7.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Generator_Courses(); - - } - - /** - * Get raw data as an array - * - * @since 4.7.0 - * - * @return void - */ - protected function get_raw( $file = 'import-with-quiz.json' ) { - - global $lifterlms_tests; - return json_decode( file_get_contents( $lifterlms_tests->assets_dir . $file ), true ); - - } - - /** - * Test add_course_terms() - * - * @since 4.7.0 - * - * @return void - */ - public function test_add_course_terms() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $raw = array( - 'categories' => array( 'cat term' ), - 'difficulty' => array( 'difficulty term', '' ), - 'tags' => array( 'tags term', 'another tag' ), - 'tracks' => array( 'tracks term' ), - ); - - $actions = did_action( 'llms_generator_new_term' ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'add_course_terms', array( $course_id, $raw ) ); - - // 5 terms created. - $this->assertEquals( 5, did_action( 'llms_generator_new_term' ) ); - - // Match. - $this->assertEqualSets( $raw['categories'], wp_get_post_terms( $course_id, 'course_cat', array( 'fields' => 'names' ) ) ); - $this->assertEqualSets( array( $raw['difficulty'][0] ), wp_get_post_terms( $course_id, 'course_difficulty', array( 'fields' => 'names' ) ) ); - $this->assertEqualSets( $raw['tags'], wp_get_post_terms( $course_id, 'course_tag', array( 'fields' => 'names' ) ) ); - $this->assertEqualSets( $raw['tracks'], wp_get_post_terms( $course_id, 'course_track', array( 'fields' => 'names' ) ) ); - - - } - - /** - * Test clone_course() - * - * @since 4.13.0 - * - * @return void - */ - public function test_clone_course() { - - $raw = array( - 'title' => 'Sample Course', - 'content' => 'Content', - ); - - $id = $this->main->clone_course( $raw ); - $this->assertTrue( is_numeric( $id ) ); - $post = get_post( $id ); - $this->assertEquals( 'course', $post->post_type ); - $this->assertEquals( 'Sample Course (Clone)', $post->post_title ); - $this->assertEquals( 'Content', $post->post_content ); - $this->assertEquals( 'draft', $post->post_status ); - - } - - /** - * Test clone_lesson() - * - * @since 4.7.0 - * @since 4.13.0 Add check against post status. - * - * @return void - */ - public function test_clone_lesson() { - - $raw = array( - 'title' => 'Sample Lesson', - 'content' => 'Content', - ); - - $id = $this->main->clone_lesson( $raw ); - $this->assertTrue( is_numeric( $id ) ); - $post = get_post( $id ); - $this->assertEquals( 'lesson', $post->post_type ); - $this->assertEquals( 'Sample Lesson (Clone)', $post->post_title ); - $this->assertEquals( 'Content', $post->post_content ); - $this->assertEquals( 'draft', $post->post_status ); - - } - - /** - * Test generate_course() - * - * @since 4.7.0 - * - * @return void - */ - public function test_generate_course() { - - $raw = $this->get_raw(); - $res = $this->main->generate_course( $raw ); - $this->assertTrue( is_numeric( $res ) ); - $this->assertEquals( 'course', get_post_type( $res ) ); - - } - - /** - * Test generate_courses() when with missing raw course data - * - * @since 4.7.0 - * - * @return void - */ - public function test_generate_courses_missing_courses() { - - $this->setExpectedException( Exception::class, 'Raw data is missing the required "courses" array.', 2000 ); - $this->main->generate_courses( array() ); - - } - - /** - * Test generate_courses() when with invalid raw course data - * - * @since 4.7.0 - * - * @return void - */ - public function test_generate_courses_invalid_courses() { - - $this->setExpectedException( Exception::class, 'The raw "courses" item must be an array.', 2001 ); - $this->main->generate_courses( array( 'courses' => 'invalid' ) ); - - } - - /** - * Test generate_courses() - * - * @since 4.7.0 - * - * @return void - */ - public function test_generate_courses() { - - $res = $this->main->generate_courses( array( - 'courses' => array( - array( - 'title' => 'Course 0', - ), - array( - 'title' => 'Course 1', - ), - ), - ) ); - - foreach ( $res as $i => $id ) { - $this->assertEquals( 'course', get_post_type( $id ) ); - $this->assertEquals( sprintf( 'Course %d', $i ), get_the_title( $id ) ); - } - - } - - /** - * Test create_access_plan() - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_access_plan() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $raw = array( - 'id' => 987, - 'author' => array( - 'id' => $this->factory->user->create(), - ), - 'title' => 'Generated Access Plan', - 'content' => 'Content', - 'status' => 'publish', - 'access_expiration' => 'lifetime', - 'availability' => 'open', - 'enroll_text' => 'Join', - 'is_free' => 'yes', - ); - - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_access_plan', array( $raw, $course_id ) ); - $plan = llms_get_post( $id ); - - $this->assertTrue( $plan instanceof LLMS_Access_Plan ); - $this->assertEquals( 'llms_access_plan', get_post_type( $id ) ); - - $this->assertEquals( $raw['author']['id'], $plan->get( 'author' ) ); - $this->assertEquals( $raw['title'], $plan->get( 'title' ) ); - $this->assertEquals( $raw['content'], $plan->get( 'content', true ) ); - $this->assertEquals( $raw['status'], $plan->get( 'status' ) ); - $this->assertEquals( $raw['access_expiration'], $plan->get( 'access_expiration' ) ); - $this->assertEquals( $raw['availability'], $plan->get( 'availability' ) ); - $this->assertEquals( $raw['enroll_text'], $plan->get( 'enroll_text' ) ); - $this->assertEquals( $raw['is_free'], $plan->get( 'is_free' ) ); - $this->assertEquals( $raw['id'], $plan->get( 'generated_from_id' ) ); - - } - - /** - * Test create_course() - * - * @since 4.7.0 - * @since 4.12.0 Only test properties that exist on the raw data arrays. - * - * @return void - */ - public function test_create_course() { - - $course_actions = did_action( 'llms_generator_new_course' ); - $plan_actions = did_action( 'llms_generator_new_access_plan' ); - $section_actions = did_action( 'llms_generator_new_section' ); - $lesson_actions = did_action( 'llms_generator_new_lesson' ); - $quiz_actions = did_action( 'llms_generator_new_quiz' ); - $question_actions = did_action( 'llms_generator_new_question' ); - - $raw = $this->get_raw(); - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_course', array( $raw ) ); - $course = llms_get_post( $id ); - - $this->assertTrue( $course instanceof LLMS_Course ); - - // Default post properties. - $this->assertEquals( $raw['title'], $course->get( 'title' ) ); - $this->assertEquals( $raw['content'], $course->get( 'content', true ) ); - - // Store the original ID. - $this->assertEquals( $raw['id'], $course->get( 'generated_from_id' ) ); - - // Test meta props are set. - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $course, 'properties' ) ) as $prop ) { - if ( isset( $raw[ $prop ] ) ) { - $this->assertEquals( $raw[ $prop ], $course->get( $prop ) ); - } - } - - // Test custom values. - foreach ( $raw['custom'] as $key => $vals ) { - $this->assertEquals( $vals, get_post_meta( $course->get( 'id' ), $key ) ); - } - - // Check taxonomies. - $this->assertEquals( $raw['difficulty'], $course->get_difficulty() ); - $this->assertEquals( $raw['categories'], $course->get_categories() ); - $this->assertEquals( $raw['tags'], $course->get_tags() ); - $this->assertEquals( $raw['tracks'], $course->get_tracks() ); - - // Calls actions (noting that children have been created). - $this->assertEquals( ++$course_actions, did_action( 'llms_generator_new_course' ) ); - $this->assertEquals( ++$plan_actions, did_action( 'llms_generator_new_access_plan' ) ); - $this->assertEquals( ++$section_actions, did_action( 'llms_generator_new_section' ) ); - $this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) ); - $this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) ); - $this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) ); - - // Test course structure of generated course is preserved. - foreach ( $course->get_sections() as $section ) { - - $this->assertEquals( $id, $section->get( 'parent_course' ) ); - - foreach ( $section->get_lessons() as $lesson ) { - - $this->assertEquals( $id, $lesson->get( 'parent_course' ) ); - $this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) ); - - $quiz = $lesson->get_quiz(); - $this->assertEquals( $lesson->get( 'id' ), $quiz->get( 'lesson_id' ) ); - - } - } - - } - - /** - * Test create_course() error. - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_course_error() { - - // Force post creation to fail. - $handler = function( $args ) { - return array(); - }; - add_filter( 'llms_new_course', $handler ); - - $this->setExpectedException( Exception::class, 'Error creating the course post object.', 1000 ); - LLMS_Unit_Test_Util::call_method( $this->main, 'create_course', array( array( 'title' => '' ) ) ); - - remove_filter( 'llms_new_course', $handler ); - - } - - /** - * Test create_lesson() - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_lesson() { - - $lesson_actions = did_action( 'llms_generator_new_lesson' ); - $quiz_actions = did_action( 'llms_generator_new_quiz' ); - $question_actions = did_action( 'llms_generator_new_question' ); - - $raw = $this->get_raw()['sections'][0]['lessons'][0]; - $order = 3; - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 0 ) ); - $section = $course->get_sections()[0]; - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_lesson', array( $raw, $order, $section->get( 'id' ), $course->get( 'id' ) ) ); - $lesson = llms_get_post( $id ); - - $this->assertTrue( $lesson instanceof LLMS_Lesson ); - - // Default post properties. - $this->assertEquals( $raw['title'], $lesson->get( 'title' ) ); - $this->assertEquals( $raw['content'], $lesson->get( 'content', true ) ); - - // Test meta props are set. - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $lesson, 'properties' ) ) as $prop ) { - // This data is not based off raw. - if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz' ), true ) ) { - continue; - } - $this->assertEquals( $raw[ $prop ], $lesson->get( $prop ), $prop ); - } - - // Test custom values. - foreach ( $raw['custom'] as $key => $vals ) { - $this->assertEquals( $vals, get_post_meta( $lesson->get( 'id' ), $key ) ); - } - - // Order. - $this->assertEquals( $order, $lesson->get( 'order' ) ); - - // Store the original ID. - $this->assertEquals( $raw['id'], $lesson->get( 'generated_from_id' ) ); - - // Calls actions (noting that children have been created). - $this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) ); - $this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) ); - $this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) ); - - // Relationships. - $this->assertEquals( $course->get( 'id' ), $lesson->get( 'parent_course' ) ); - $this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) ); - - $quiz = $lesson->get_quiz(); - $this->assertEquals( $id, $quiz->get( 'lesson_id' ) ); - - } - - public function test_create_quiz() { - - $quiz_actions = did_action( 'llms_generator_new_quiz' ); - $question_actions = did_action( 'llms_generator_new_question' ); - - $raw = $this->get_raw()['sections'][0]['lessons'][0]['quiz']; - $lesson_id = $this->factory->post->create( array( 'post_type' => 'lesson' ) ); - $raw['lesson_id'] = $lesson_id; - - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_quiz', array( $raw ) ); - $quiz = llms_get_post( $id ); - - $this->assertTrue( $quiz instanceof LLMS_Quiz ); - - // Default post properties. - $this->assertEquals( $raw['title'], $quiz->get( 'title' ) ); - $this->assertEquals( $raw['content'], $quiz->get( 'content', true ) ); - - // Test meta props are set. - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $quiz, 'properties' ) ) as $prop ) { - // This data is not based off raw. - if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz' ), true ) ) { - continue; - } - $this->assertEquals( $raw[ $prop ], $quiz->get( $prop ), $prop ); - } - - // Test custom values. - foreach ( $raw['custom'] as $key => $vals ) { - $this->assertEquals( $vals, get_post_meta( $quiz->get( 'id' ), $key ) ); - } - - // Store the original ID. - $this->assertEquals( $raw['id'], $quiz->get( 'generated_from_id' ) ); - - // Calls actions (noting that children have been created). - $this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) ); - $this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) ); - - // Relationships. - $this->assertEquals( $lesson_id, $quiz->get( 'lesson_id' ) ); - - } - - /** - * Test create_question() - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_question() { - - $question_actions = did_action( 'llms_generator_new_question' ); - - $raw = $this->get_raw()['sections'][0]['lessons'][0]['quiz']['questions'][0]; - $quiz_id = $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) ); - $quiz = llms_get_post( $quiz_id ); - - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_question', array( $raw, $quiz->questions(), $this->factory->user->create() ) ); - $question = llms_get_post( $id ); - - $this->assertTrue( $question instanceof LLMS_Question ); - - // Default post properties. - $this->assertEquals( $raw['title'], $question->get( 'title' ) ); - $this->assertEquals( $raw['content'], $question->get( 'content', true ) ); - - // Test meta props are set. - foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $question, 'properties' ) ) as $prop ) { - // This data is not based off raw. - if ( in_array( $prop, array( 'parent_id' ), true ) ) { - continue; - } - $this->assertEquals( $raw[ $prop ], $question->get( $prop ), $prop ); - } - - // Store the original ID. - $this->assertEquals( $raw['id'], $question->get( 'generated_from_id' ) ); - - // Calls actions (noting that children have been created). - $this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) ); - - // Relationships. - $this->assertEquals( $quiz_id, $question->get( 'parent_id' ) ); - - // Check choices. - foreach ( $question->get_choices() as $i => $choice ) { - - $this->assertEquals( $id, $choice->get_question_id() ); - - $this->assertEquals( $raw['choices'][ $i ]['choice'], $choice->get_choice() ); - $this->assertEquals( $raw['choices'][ $i ]['choice_type'], $choice->get( 'choice_type' ) ); - $this->assertEquals( $raw['choices'][ $i ]['correct'], $choice->get( 'correct' ) ); - $this->assertEquals( $raw['choices'][ $i ]['marker'], $choice->get( 'marker' ) ); - - } - - } - - /** - * Test create_question() during a post creation error - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_question_error() { - - $quiz_id = $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) ); - $quiz = llms_get_post( $quiz_id ); - - // Force post creation to fail. - $handler = function( $args ) { - return array(); - }; - add_filter( 'llms_new_question', $handler ); - - $this->setExpectedException( Exception::class, 'Error creating the question post object.', 1000 ); - LLMS_Unit_Test_Util::call_method( $this->main, 'create_question', array( array( 'title' => '' ), $quiz->questions(), $this->factory->user->create() ) ); - - remove_filter( 'llms_new_question', $handler ); - - } - - /** - * Test create_section() - * - * @since 4.7.0 - * - * @return void - */ - public function test_create_section() { - - $section_actions = did_action( 'llms_generator_new_section' ); - $lesson_actions = did_action( 'llms_generator_new_lesson' ); - $quiz_actions = did_action( 'llms_generator_new_quiz' ); - $question_actions = did_action( 'llms_generator_new_question' ); - - $raw = $this->get_raw()['sections'][0]; - $order = 20; - $course = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $id = LLMS_Unit_Test_Util::call_method( $this->main, 'create_section', array( $raw, $order, $course ) ); - $section = llms_get_post( $id ); - - $this->assertTrue( $section instanceof LLMS_Section ); - - // Default post properties. - $this->assertEquals( $raw['title'], $section->get( 'title' ) ); - - // These are the only important pieces of meta data. - $this->assertEquals( $course, $section->get( 'parent_course' ) ); - $this->assertEquals( $order, $section->get( 'order' ) ); - - // Store the original ID. - $this->assertEquals( $raw['id'], $section->get( 'generated_from_id' ) ); - - // Calls actions (noting that children have been created). - $this->assertEquals( ++$section_actions, did_action( 'llms_generator_new_section' ) ); - $this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) ); - $this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) ); - $this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) ); - - // Test course structure of generated course is preserved. - foreach ( $section->get_lessons() as $lesson ) { - - $this->assertEquals( $course, $lesson->get( 'parent_course' ) ); - $this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) ); - - $quiz = $lesson->get_quiz(); - $this->assertEquals( $lesson->get( 'id' ), $quiz->get( 'lesson_id' ) ); - - } - - } - - /** - * Test handle_prerequisites() - * - * @since 4.7.0 - * - * @return void - */ - public function test_handle_prerequisites() { - - $raw = $this->get_raw( 'import-with-prerequisites.json' ); - $courses = $this->main->generate_courses( $raw ); - - $course = llms_get_post( $courses[0] ); - - $this->assertTrue( $course->has_prerequisite( 'course' ) ); - $this->assertEquals( $courses[1], $course->get_prerequisite_id( 'course' ) ); - - // Tracks aren't preserved. - $this->assertFalse( $course->has_prerequisite( 'course_track' ) ); - - $lessons = $course->get_lessons(); - $this->assertTrue( $lessons[1]->has_prerequisite() ); - $this->assertEquals( $lessons[0]->get( 'id' ), $lessons[1]->get_prerequisite() ); - - } - - - /** - * Test maybe_sideload_choice_image() for various conditions where the choice can't be sideloaded. - * - * @since 4.7.0 - * - * @return void - */ - public function test_maybe_sideload_choice_image_disabled() { - - $choice = array( - 'id' => 'mock', - 'choice' => 'string', - ); - - // The 'choice_type' prop is missing. - $this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) ); - - $choice['choice_type'] = 'text'; - - // The 'choice_type' prop is not "image". - $this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) ); - - // Sideloading is disabled. - add_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - $this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) ); - remove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' ); - - } - - /** - * Test maybe_sideload_choice_image() - * - * @since 4.7.0 - * - * @return void - */ - public function test_maybe_sideload_choice_image() { - - $choice = array( - 'id' => 'mock', - 'choice_type' => 'image', - 'choice' => array( - 'id' => 123, - 'src' => 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg', - ), - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ); - - $this->assertTrue( 123 !== $res['choice']['id'] ); - $this->assertTrue( $choice['choice']['src'] !== $res['choice']['src'] ); - $this->assertEquals( wp_get_attachment_url( $res['choice']['id'] ), $res['choice']['src'] ); - - } - - - /** - * Test maybe_sideload_choice_image() when an error is encountered during sideloading - * - * @since 4.7.0 - * - * @return void - */ - public function test_maybe_sideload_choice_image_error() { - - $choice = array( - 'id' => 'mock', - 'choice_type' => 'image', - 'choice' => array( - 'id' => 123, - 'src' => 'fake.jpg', - ), - ); - - $this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) ); - - } - - /** - * Test store_temp_id() - * - * @since 4.7.0 - * - * @return void - */ - public function test_store_temp_id() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $course = llms_get_post( $course_id ); - - $raw = array( - 'id' => 128, - ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'store_temp_id', array( $raw, $course ) ); - - $this->assertEquals( 128, $res ); - $this->assertEquals( 128, $course->get( 'generated_from_id' ) ); - - $this->assertEquals( array( 128 => $course_id ), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'tempids' )['course'] ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-generator.php b/tests/phpunit/unit-tests/class-llms-test-generator.php deleted file mode 100644 index 8bdebb9d64..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-generator.php +++ /dev/null @@ -1,459 +0,0 @@ -<?php -/** - * LLMS Generator Tests - * - * @group generator - * - * @since Unknown - * @since 3.36.3 Add tests for `is_generator_valid()` and `set_generator()` methods. - * Split `is_error()` method tests into multiple tests. - * @since 3.37.4 Don't test against core metadata. - * @since 4.7.0 Add tests for image sideloading methods. - */ -class LLMS_Test_Generator extends LLMS_UnitTestCase { - - /** - * Test generate method. - * - * @since Unknown. - * @since 3.37.4 Don't test against core metadata. - * @since 4.7.0 Update to accommodate changes in results data (and test to maintain backwards compat). - * @since 5.0.0 Ignore core custom field data for custom data assertions. - * - * @return void - */ - public function test_generate() { - - $course = $this->get_mock_course_array( 1, 3, 5, 1, 5 ); - - $course['author'] = array( - 'email' => 'test@test.tld', - 'id' => 12345, - ); - $course['categories'] = array( 'cat' ); - $course['tags'] = array( 'tag1', 'tag2' ); - $course['tracks'] = array( 'track' ); - $course['difficulty'] = 'hard'; - $course['access_plans'] = array( - array( - 'title' => 'plan1' - ), - array( - 'title' => 'plan2' - ), - ); - - $course['custom'] = array( - 'customdata' => array( 'yes' ), - 'customdata2' => array( 'no', 'yes', 'maybe' ), - 'customdata3' => array( serialize( array( 'no', 'yes', 'maybe' ) ) ), - ); - - $gen = new LLMS_Generator( $course ); - $gen->set_generator( 'LifterLMS/SingleCourseGenerator' ); - $gen->set_default_post_status( 'publish' ); - $gen->generate(); - - $results = $gen->get_results(); - - // Backwards compat keys. - $this->assertEquals( 1, $results['authors'] ); - $this->assertEquals( 1, $results['courses'] ); - $this->assertEquals( 3, $results['sections'] ); - $this->assertEquals( 15, $results['lessons'] ); - $this->assertEquals( 3, $results['quizzes'] ); - $this->assertEquals( 15, $results['questions'] ); - $this->assertEquals( 5, $results['terms'] ); - $this->assertEquals( 2, $results['plans'] ); - - // Everything else. - $this->assertEquals( 1, $results['user'] ); - $this->assertEquals( 1, $results['course'] ); - $this->assertEquals( 3, $results['section'] ); - $this->assertEquals( 15, $results['lesson'] ); - $this->assertEquals( 3, $results['quiz'] ); - $this->assertEquals( 15, $results['question'] ); - $this->assertEquals( 5, $results['term'] ); - $this->assertEquals( 2, $results['access_plan'] ); - $this->assertEquals( 1, $results['user'] ); - - // Ensure custom data is properly added - $courses = $gen->get_generated_courses(); - $custom = get_post_custom( $courses[0] ); - unset( $custom['_llms_instructors'] ); // Ignore core custom data. - $this->assertEquals( $course['custom'], $custom ); - - } - - /** - * Test get_error_code(). - * - * @since 4.9.0 - * - * @return void - */ - public function test_get_error_code() { - - $gen = new LLMS_Generator( array() ); - $class = new LLMS_Generator_Courses(); - - $errors = array( - - // Native errors. - E_ERROR => 'E_ERROR', - E_COMPILE_ERROR => 'E_COMPILE_ERROR', - - // From Courses generator class. - 2000 => 'ERROR_GEN_MISSING_REQUIRED', - 2001 => 'ERROR_GEN_INVALID_FORMAT', - - // From posts generator abstract. - 1000 => 'ERROR_CREATE_POST', - 1001 => 'ERROR_CREATE_TERM', - 1002 => 'ERROR_CREATE_USER', - 1100 => 'ERROR_INVALID_POST', - - // Undefined error. - 9999 => 'ERROR_UNKNOWN', - - ); - - foreach ( $errors as $in => $out ) { - $this->assertEquals( $out, LLMS_Unit_Test_Util::call_method( $gen, 'get_error_code', array( $in, $class ) ) ); - } - - } - - /** - * Test get_results() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_results() { - - $gen = new LLMS_Generator( array() ); - $expect = array( - 'courses' => 0, - 'sections' => 0, - 'lessons' => 0, - 'plans' => 0, - 'quizzes' => 0, - 'questions' => 0, - 'terms' => 0, - 'authors' => 0, - ); - $this->assertEquals( $expect, $gen->get_results() ); - - } - - /** - * Test get_results() when an error is encountered - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_results_error() { - - $gen = new LLMS_Generator( array() ); - $gen->generate(); - $res = $gen->get_results(); - $this->assertIsWPError($res ); - $this->assertWPErrorCodeEquals( 'missing-generator', $res ); - - } - - /** - * Test get_generated_content() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_generated_content() { - - $expect = array( 'mock' => array( 1 ) ); - $gen = new LLMS_Generator( array() ); - LLMS_Unit_Test_Util::set_private_property( $gen, 'generated', $expect ); - - $this->assertEquals( $expect, $gen->get_generated_content() ); - - } - - /** - * Test get_generated_courses() - * - * @since 4.7.0 - * - * @return void - */ - public function test_get_generated_courses() { - - $gen = new LLMS_Generator( array() ); - - // No courses. - $this->assertEquals( array(), $gen->get_generated_courses() ); - - LLMS_Unit_Test_Util::set_private_property( $gen, 'generated', array( 'course' => array( 123 ) ) ); - $this->assertEquals( array( 123 ), $gen->get_generated_courses() ); - - } - - /** - * Test get_generated_posts() - * - * @since 4.7.0 - * - * @expectedDeprecated LLMS_Generator::get_generated_posts() - * - * @return void - */ - public function test_get_generated_posts() { - - $gen = new LLMS_Generator( array() ); - $gen->get_generated_posts(); - - } - - /** - * Test is_error() method: no generator supplied. - * - * @since 3.36.3 - * @since 4.7.0 Added assertion for error code. - * - * @return void - */ - public function test_is_error_no_generator() { - - $gen = new LLMS_Generator( array() ); - $gen->generate(); - $this->assertTrue( $gen->is_error() ); - $this->assertWPErrorCodeEquals( 'missing-generator', $gen->error ); - - } - - /** - * Test is_error() method: valid generator but no data to generate. - * - * @since 3.36.3 - * @since 4.7.0 Added assertion for error code. - * - * @return void - */ - public function test_is_error_no_data() { - - $gen = new LLMS_Generator( array() ); - $gen->set_generator( 'LifterLMS/BulkCourseGenerator' ); - $gen->generate(); - $this->assertTrue( $gen->is_error() ); - $this->assertWPErrorCodeEquals( 'ERROR_GEN_MISSING_REQUIRED', $gen->error ); - - } - - /** - * Test is_error() method: valid generator but data formatted improperly. - * - * @since 3.36.3 - * @since 4.7.0 Added assertion for error code. - * - * @return void - */ - public function test_is_error_invalid_data_format() { - - $gen = new LLMS_Generator( array( 'title' => 'course title' ) ); - $gen->set_generator( 'LifterLMS/BulkCourseGenerator' ); - $gen->generate(); - $this->assertTrue( $gen->is_error() ); - $this->assertWPErrorCodeEquals( 'ERROR_GEN_MISSING_REQUIRED', $gen->error ); - - } - - /** - * Test is_error() method: not an error - * - * @since 3.36.3 - * - * @return void - */ - public function test_is_error_not_an_error() { - - $gen = new LLMS_Generator( array( 'title' => 'course title' ) ); - $gen->set_generator( 'LifterLMS/SingleCourseExporter' ); - $gen->generate(); - $this->assertFalse( $gen->is_error() ); - - } - - /** - * Test is_generator_valid() method: valid generators. - * - * @since 3.36.3 - * - * @return void - */ - public function test_is_generator_valid_valid_generators() { - - $gen = new LLMS_Generator( array() ); - $list = array_keys( LLMS_Unit_Test_Util::call_method( $gen, 'get_generators' ) ); - foreach ( $list as $name ) { - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( $name ) ) ); - } - - } - - /** - * Test is_generator_valid() method: invalid generators. - * - * @since 3.36.3 - * - * @return void - */ - public function test_is_generator_valid_invalid() { - - $gen = new LLMS_Generator( array() ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( 'fake' ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( 'LifterLMS/SingleFakeExporter' ) ) ); - - } - - /** - * Test parse_raw() when passing in an array - * - * @since 4.7.0 - * - * @return void - */ - public function test_parse_raw_array() { - - $gen = new LLMS_Generator( array() ); - $this->assertEquals( array( 'test' ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( array( 'test' ) ) ) ); - - } - - /** - * Test parse_raw() when passing in a JSON string - * - * @since 4.7.0 - * - * @return void - */ - public function test_parse_raw_json() { - - $gen = new LLMS_Generator( array() ); - $this->assertEquals( array( 'test' ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( wp_json_encode( array( 'test' ) ) ) ) ); - - } - - /** - * Test parse_raw() when passing in an object - * - * @since 4.7.0 - * - * @return void - */ - public function test_parse_raw_object() { - - $gen = new LLMS_Generator( array() ); - $obj = new stdClass(); - $obj->test = 1; - $this->assertEquals( array( 'test' => 1 ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( wp_json_encode( $obj ) ) ) ); - - } - - /** - * Test parse_raw() when passing in invalid data - * - * @since 4.7.0 - * - * @return void - */ - public function test_parse_raw_invalid() { - - $gen = new LLMS_Generator( array() ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( 'not json string' ) ) ); - - } - - /** - * Test set_generator(): interpret from raw missing generator. - * - * @since 3.36.3 - * - * @return void - */ - public function test_set_generator_interpret_from_raw_missing() { - - $gen = new LLMS_Generator( array() ); - $err = $gen->set_generator(); - $this->assertIsWPError( $err ); - $this->assertWPErrorCodeEquals( 'missing-generator', $err ); - - } - - /** - * Test set_generator(): interpret from raw invalid generator. - * - * @since 3.36.3 - * - * @return void - */ - public function test_set_generator_interpret_from_raw_invalid() { - - $gen = new LLMS_Generator( array( - '_generator' => 'Fake/Generator', - ) ); - $err = $gen->set_generator(); - $this->assertIsWPError( $err ); - $this->assertWPErrorCodeEquals( 'invalid-generator', $err ); - - } - - /** - * Test set_generator(): interpret from raw success. - * - * @since 3.36.3 - * - * @return void - */ - public function test_set_generator_interpret_from_raw_success() { - - $gen = new LLMS_Generator( array( - '_generator' => 'LifterLMS/SingleCourseExporter', - ) ); - $this->assertEquals( 'LifterLMS/SingleCourseExporter', $gen->set_generator() ); - - } - - /** - * Test set_generator(): explicitly supplied invalid. - * - * @since 3.36.3 - * - * @return void - */ - public function test_set_generator_explicit_invalid() { - - $gen = new LLMS_Generator( array() ); - $err = $gen->set_generator( 'Fake/Generator' ); - $this->assertIsWPError( $err ); - $this->assertWPErrorCodeEquals( 'invalid-generator', $err ); - - } - - /** - * Test set_generator(): explicitly supplied success. - * - * @since 3.36.3 - * - * @return void - */ - public function test_set_generator_explicit_success() { - - $gen = new LLMS_Generator( array() ); - $this->assertEquals( 'LifterLMS/SingleCourseExporter', $gen->set_generator( 'LifterLMS/SingleCourseExporter' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-grades.php b/tests/phpunit/unit-tests/class-llms-test-grades.php deleted file mode 100644 index d0a9352c27..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-grades.php +++ /dev/null @@ -1,186 +0,0 @@ -<?php -/** - * Tests for Grading methods - * @group grades - * @since 3.24.0 - */ -class LLMS_Test_Grades extends LLMS_UnitTestCase { - - /** - * Test the `instance()` method. - * - * @since 3.24.0 - * @since 5.3.0 Rename `_instance` property to `instance`. - * - * @return void - */ - public function test_instance() { - - $this->assertTrue( is_a( LLMS_Grades::instance(), 'LLMS_Grades' ) ); - $this->assertClassHasStaticAttribute( 'instance', 'LLMS_Grades' ); - - } - - /** - * test calculate_grade() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_calculate_grade() { - - $grader = LLMS()->grades(); - - $student = $this->get_mock_student(); - $course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] ); - - $student->enroll( $course->get( 'id' ) ); - - // no grade yet - $this->assertNull( $grader->calculate_grade( $course, $student ) ); - - $possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ); - $lesson_points = array(); - $lesson_grades = array(); - - foreach ( $course->get_lessons() as $i => $lesson ) { - - // calculate the ongoing grade as quizzes are completed - if ( 0 !== $i ) { - $total_points = array_sum( $lesson_points ); - $course_grade = 0; - foreach ( $lesson_grades as $i => $grade ) { - if ( $lesson_points[ $i ] ) { - $course_grade += $grade * ( $lesson_points[ $i ] / $total_points ); - } - } - $this->assertEquals( round( $course_grade, 2 ), $grader->calculate_grade( $course, $student ) ); - } - - $points = rand( 0, 5 ); - $lesson->set( 'points', $points ); - $lesson_points[] = $points; - - // no grade on the lesson yet - $this->assertNull( $grader->calculate_grade( $lesson, $student ) ); - - $quiz_id = $lesson->get( 'quiz' ); - if ( ! $quiz_id ) { - continue; - } - - $grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ]; - $this->take_quiz( $quiz_id, $student->get( 'id' ), $grade ); - $this->assertEquals( $grade, $grader->calculate_grade( $lesson, $student ) ); - $lesson_grades[] = $grade; - - } - - $total_points = array_sum( $lesson_points ); - $course_grade = 0; - foreach ( $lesson_grades as $i => $grade ) { - if ( $lesson_points[ $i ] ) { - $course_grade += $grade * ( $lesson_points[ $i ] / $total_points ); - } - } - - // checkout overall course grade once completed - $this->assertEquals( round( $course_grade, 2 ), $grader->calculate_grade( $course, $student ) ); - - } - - /** - * test get_grade() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_grade() { - - $grader = LLMS()->grades(); - - $student = $this->get_mock_student(); - $course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] ); - - $student->enroll( $course->get( 'id' ) ); - - // no grade yet - $this->assertNull( $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) ); - - $possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ); - $lesson_points = array(); - $lesson_grades = array(); - - foreach ( $course->get_lessons() as $i => $lesson ) { - - // calculate the ongoing grade as quizzes are completed - if ( 0 !== $i ) { - $total_points = array_sum( $lesson_points ); - $course_grade = 0; - foreach ( $lesson_grades as $i => $grade ) { - if ( $lesson_points[ $i ] ) { - $course_grade += $grade * ( $lesson_points[ $i ] / $total_points ); - } - } - $this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ), false ) ); - $this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) ); - } - - $points = rand( 0, 5 ); - $lesson->set( 'points', $points ); - $lesson_points[] = $points; - - // no grade on the lesson yet - $this->assertNull( $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) ); - - $quiz_id = $lesson->get( 'quiz' ); - if ( ! $quiz_id ) { - continue; - } - - $grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ]; - $this->take_quiz( $quiz_id, $student->get( 'id' ), $grade ); - $this->assertNull( $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) ); // cached - $this->assertEquals( $grade, $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ), false ) ); // no cache - $this->assertEquals( $grade, $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) ); // cached - $lesson_grades[] = $grade; - - } - - $total_points = array_sum( $lesson_points ); - $course_grade = 0; - foreach ( $lesson_grades as $i => $grade ) { - if ( $lesson_points[ $i ] ) { - $course_grade += $grade * ( $lesson_points[ $i ] / $total_points ); - } - } - - // checkout overall course grade once completed - $this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ), false ) ); - $this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) ); - - - } - - /** - * test round() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_round() { - - $this->assertEquals( 0, LLMS()->grades()->round( 0 ) ); - $this->assertEquals( 1.5, LLMS()->grades()->round( 1.5 ) ); - $this->assertEquals( 25, LLMS()->grades()->round( 25 ) ); - $this->assertEquals( 25.0, LLMS()->grades()->round( 25.0 ) ); - $this->assertEquals( 1.67, LLMS()->grades()->round( 1.666 ) ); - $this->assertEquals( 251.67, LLMS()->grades()->round( 251.666 ) ); - $this->assertEquals( 82.12, LLMS()->grades()->round( 82.123 ) ); - $this->assertEquals( 98.13, LLMS()->grades()->round( 98.125 ) ); - $this->assertEquals( 75.12, LLMS()->grades()->round( 75.12 ) ); - $this->assertEquals( 0.02, LLMS()->grades()->round( 0.015559 ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-hasher.php b/tests/phpunit/unit-tests/class-llms-test-hasher.php deleted file mode 100644 index 2cf7b81874..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-hasher.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Tests for LLMS_Hasher - * - * @group hasher - * - * @version 3.16.10 - */ -class LLMS_Test_Hasher extends LLMS_UnitTestCase { - - private $ids = array(); - private $hashes = array(); - - private function get_random_id( $max = 99999999 ) { - - $id = rand( 1, $max ); - while ( ! in_array( $id, $this->ids ) ) { - array_push( $this->ids, $id ); - return $id; - } - return $max + 1; - - } - - /** - * Test the hashing/unhashing functions - * - * @since 3.16.10 - * - * @return void - */ - public function test_hash_unhash() { - - foreach ( range( 1, 10000 ) as $i ) { - $id = $this->get_random_id(); - $hash = LLMS_Hasher::hash( $id ); - $this->assertFalse( in_array( $hash, $this->hashes ) ); - $this->assertEquals( $id, LLMS_Hasher::unhash( $hash ) ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-helper.php b/tests/phpunit/unit-tests/class-llms-test-helper.php deleted file mode 100644 index f52d070378..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-helper.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Test inclusion and initialization of the helper library. - * - * @package LifterLMS/Tests - * - * @group helper - * @group packages - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Helper extends LLMS_Unit_Test_Case { - - /** - * Test helper lib exists and is loaded. - * - * @since 5.0.0 - * - * @return void - */ - public function test_helper_lib_exists() { - $this->assertTrue( class_exists( 'LifterLMS_Helper' ) ); - $this->assertTrue( defined( 'LLMS_HELPER_VERSION' ) ); - $this->assertNotNull( LLMS_HELPER_VERSION ); - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-https.php b/tests/phpunit/unit-tests/class-llms-test-https.php deleted file mode 100644 index 0dbca0dcd9..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-https.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php -/** - * Test LLMS_HTTPS class - * - * @package LifterLMS/Tests - * - * @group https - * - * @since 3.35.1 - * @version 3.35.1 - */ -class LLMS_Test_HTTPS extends LLMS_UnitTestCase { - - /** - * Setup testcase. - * - * @since 3.35.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->https = new LLMS_HTTPS(); - $this->original_server = $_SERVER; - - } - - /** - * Setup testcase. - * - * @since 3.35.1 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - $_SERVER = $this->original_server; - - } - - /** - * Test force url getter - * - * @since 3.35.1 - * - * @return void - */ - public function test_get_force_redirect_url() { - - // No REQUEST_URI or HTTP_X_FORWARDED_HOST. - $this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) ); - $this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) ); - - // No REQUEST_URI. - $_SERVER['HTTP_X_FORWARDED_HOST'] = 'example.org'; - $this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) ); - $this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) ); - - $_SERVER['REQUEST_URI'] = 'http://example.org'; - $this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) ); - - $_SERVER['REQUEST_URI'] = 'https://example.org'; - $this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) ); - - $_SERVER['REQUEST_URI'] = 'https://example.org'; - $this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) ); - - $_SERVER['REQUEST_URI'] = 'http://example.org'; - $this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) ); - - } - - /** - * Test force redirect - * - * @since 3.35.1 - * - * @return void - */ - public function test_force_https_redirect() { - - LLMS_Install::create_pages(); - - $_SERVER['HTTPS'] = 1; - $this->assertNull( $this->https->force_https_redirect() ); - - unset( $_SERVER['HTTPS'] ); - $url = llms_get_page_url( 'checkout' ); - $this->go_to( $url ); - - $this->expectException( LLMS_Unit_Test_Exception_Exit::class ); - $this->expectExceptionMessage( sprintf( '%s [301] YES', preg_replace( '|^http://|', 'https://', $url ) ) ); - - $this->https->force_https_redirect(); - - } - - /** - * Test unforce redirect - * - * @since 3.35.1 - * - * @return void - */ - public function test_unforce_https_redirect() { - - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'checkout' ) ); - - $home = get_option( 'home' ); - update_option( 'home', preg_replace( '|^https://|', 'http://', $home ) ); - $this->assertNull( $this->https->unforce_https_redirect() ); - update_option( 'home', $home ); - - $_SERVER['HTTPS'] = 1; - $this->assertNull( $this->https->unforce_https_redirect() ); - - $this->go_to( '/' ); - unset( $_SERVER['HTTPS'] ); - $this->assertNull( $this->https->unforce_https_redirect() ); - - $_SERVER['HTTPS'] = 1; - $this->expectException( LLMS_Unit_Test_Exception_Exit::class ); - $this->expectExceptionMessage( 'http://example.org/ [301] YES' ); - - $this->assertNull( $this->https->unforce_https_redirect() ); - - } - -} - diff --git a/tests/phpunit/unit-tests/class-llms-test-install.php b/tests/phpunit/unit-tests/class-llms-test-install.php deleted file mode 100644 index c3b2d62b06..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-install.php +++ /dev/null @@ -1,379 +0,0 @@ -<?php -/** - * Tests for the LLMS_Install Class - * - * @package LifterLMS/Tests - * - * @group install - * - * @since 3.3.1 - * @since 3.37.8 Fix directory path to uninstall.php. - * @since 4.0.0 Test creation of all tables; fix caching issue when testing full install; add new cron test. - * @since 4.5.0 Test log backup cron. - * @since 5.0.0 Added tests for the get_can_install_user_id() method. - */ -class LLMS_Test_Install extends LLMS_UnitTestCase { - - /** - * Tests for check_version() - * - * @since 3.3.1 - * - * @return void - */ - public function test_check_version() { - - // Ensure the database update runs. - update_option( 'lifterlms_current_version', (float) LLMS()->version - 1 ); - update_option( 'lifterlms_db_version', LLMS()->version ); - LLMS_Install::check_version(); - $this->assertTrue( did_action( 'lifterlms_updated' ) === 1 ); - - // Ensure that if both are equal the database doesn't run again. - update_option( 'lifterlms_current_version', LLMS()->version ); - update_option( 'lifterlms_db_version', LLMS()->version ); - LLMS_Install::check_version(); - $this->assertTrue( did_action( 'lifterlms_updated' ) === 1 ); - - } - - /** - * Tests for create_cron_jobs() - * - * @since 3.3.1 - * @since 3.28.0 Unknown. - * @since 4.0.0 Test session cleanup cron. - * @since 4.5.0 Test log backup cron. - * - * @return void - */ - public function test_create_cron_jobs() { - - $crons = array( - 'llms_cleanup_tmp', - 'llms_backup_logs', - 'llms_send_tracking_data', - 'llms_delete_expired_session_data', - ); - - // Clear. - foreach ( $crons as $cron ) { - wp_clear_scheduled_hook( $cron ); - $this->assertFalse( wp_next_scheduled( $cron ) ); - } - - LLMS_Install::create_cron_jobs(); - - // Scheduled. - foreach ( $crons as $cron ) { - $this->assertTrue( is_numeric( wp_next_scheduled( $cron ) ) ); - } - - } - - /** - * Tests for create_difficulties() & remove_difficulties() - * - * @since 3.3.1 - * - * @return void - */ - public function test_create_difficulties_crud() { - - // Terms may or may not exist and should exist after creation. - LLMS_Install::create_difficulties(); - foreach( LLMS_Install::get_difficulties() as $name ) { - $this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'course_difficulty' ) ); - } - - // Terms should not exist after deleting terms. - LLMS_Install::remove_difficulties(); - foreach( LLMS_Install::get_difficulties() as $name ) { - $this->assertFalse( get_term_by( 'name', $name, 'course_difficulty' ) ); - } - - // Terms should exist after creating difficulties. - LLMS_Install::create_difficulties(); - foreach( LLMS_Install::get_difficulties() as $name ) { - $this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'course_difficulty' ) ); - } - - } - - /** - * Test create_files() - * - * @since 3.3.1 - * - * @return void - */ - public function test_create_files() { - - LLMS_Install::create_files(); - $this->assertTrue( file_exists( LLMS_LOG_DIR ) ); - $this->assertTrue( file_exists( LLMS_LOG_DIR . '.htaccess' ) ); - $this->assertTrue( file_exists( LLMS_LOG_DIR . 'index.html' ) ); - $this->assertFalse( file_exists( LLMS_LOG_DIR . 'fail.txt' ) ); - - } - - /** - * Tests for create_options() - * - * @since 3.3.1 - * @since 3.5.1 Unknown. - * - * @return void - */ - public function test_create_options() { - - // Clear options. - global $wpdb; - $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'lifterlms\_%';" ); - - // Install options. - LLMS_Install::create_options(); - - // Check they exist. - $settings = LLMS_Admin_Settings::get_settings_tabs(); - - foreach ( $settings as $section ) { - // Skip general settings since this screen doesn't actually have any settings on it. - if ( 'general' === $section->id ) { - continue; - } - foreach ( $section->get_settings() as $value ) { - if ( isset( $value['default'] ) && isset( $value['id'] ) ) { - $this->assertEquals( $value['default'], get_option( $value['id'] ) ); - } - } - } - - } - - /** - * Tests for create_pages() - * - * @since 3.3.1 - * - * @return void - */ - public function test_create_pages() { - - // Clear options. - delete_option( 'lifterlms_shop_page_id' ); - delete_option( 'lifterlms_memberships_page_id' ); - delete_option( 'lifterlms_checkout_page_id' ); - delete_option( 'lifterlms_myaccount_page_id' ); - - LLMS_Install::create_pages(); - - $this->assertGreaterThan( 0, get_option( 'lifterlms_shop_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_memberships_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_checkout_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_myaccount_page_id' ) ); - - // Delete pages. - wp_delete_post( get_option( 'lifterlms_shop_page_id' ), true ); - wp_delete_post( get_option( 'lifterlms_memberships_page_id' ), true ); - wp_delete_post( get_option( 'lifterlms_checkout_page_id' ), true ); - wp_delete_post( get_option( 'lifterlms_myaccount_page_id' ), true ); - - // Clear options. - delete_option( 'lifterlms_shop_page_id' ); - delete_option( 'lifterlms_memberships_page_id' ); - delete_option( 'lifterlms_checkout_page_id' ); - delete_option( 'lifterlms_myaccount_page_id' ); - - LLMS_Install::create_pages(); - - $this->assertGreaterThan( 0, get_option( 'lifterlms_shop_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_memberships_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_checkout_page_id' ) ); - $this->assertGreaterThan( 0, get_option( 'lifterlms_myaccount_page_id' ) ); - - } - - /** - * Tests for create_tables() - * - * @since 3.3.1 - * @since 4.0.0 Add missing tables. - * - * @return void - */ - public function test_create_tables() { - - global $wpdb; - - $tables = array( - "{$wpdb->prefix}lifterlms_user_postmeta", - "{$wpdb->prefix}lifterlms_quiz_attempts", - "{$wpdb->prefix}lifterlms_product_to_voucher", - "{$wpdb->prefix}lifterlms_voucher_code_redemptions", - "{$wpdb->prefix}lifterlms_vouchers_codes", - "{$wpdb->prefix}lifterlms_notifications", - "{$wpdb->prefix}lifterlms_events", - "{$wpdb->prefix}lifterlms_sessions", - ); - - // Clear tables. - // $list = implode( ', ', $tables ); - // $wpdb->query( "DROP TABLE IF EXISTS $list;" ); - - // Install tables. - LLMS_Install::create_tables(); - - foreach ( $tables as $table ) { - $this->assertEquals( $table, $wpdb->get_var( "SHOW TABLES LIKE '{$table}'" ) ); - } - - } - - /** - * Test create_visibilities() - * - * @since 3.6.0 - * - * @return void - */ - public function test_create_visibilities() { - - // Terms may or may not exist and should exist after creation. - LLMS_Install::create_visibilities(); - foreach( array_keys( llms_get_product_visibility_options() ) as $name ) { - $this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'llms_product_visibility' ) ); - } - - } - - /** - * Test get_difficulties() - * - * @since 3.3.1 - * - * @return void - */ - public function test_get_difficulties() { - - $this->assertTrue( ! empty( LLMS_Install::get_difficulties() ) ); - $this->assertTrue( is_array( LLMS_Install::get_difficulties() ) ); - - } - - /** - * Test update_db_version() - * - * @since 3.3.1 - * - * @return void - */ - public function test_update_db_version() { - - LLMS_Install::update_db_version( '1' ); - $this->assertEquals( '1', get_option( 'lifterlms_db_version' ) ); - - LLMS_Install::update_db_version(); - $this->assertEquals( LLMS()->version, get_option( 'lifterlms_db_version' ) ); - - LLMS_Install::update_db_version( '1.2.3' ); - $this->assertEquals( '1.2.3', get_option( 'lifterlms_db_version' ) ); - - } - - /** - * Test update_llms_version() - * - * @since 3.3.1 - * - * @return void - */ - public function test_update_llms_version() { - - LLMS_Install::update_llms_version( '1' ); - $this->assertEquals( '1', get_option( 'lifterlms_current_version' ) ); - - LLMS_Install::update_llms_version(); - $this->assertEquals( LLMS()->version, get_option( 'lifterlms_current_version' ) ); - - LLMS_Install::update_llms_version( '1.2.3' ); - $this->assertEquals( '1.2.3', get_option( 'lifterlms_current_version' ) ); - - } - - /** - * Tests for install() function - * - * @since 3.3.1 - * @since 3.37.8 Fix directory path to uninstall.php - * @since 4.0.0 Flush cache after uninstall is run. - * - * @return void - */ - public function test_install() { - - // Clean existing install first. - if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { - define( 'WP_UNINSTALL_PLUGIN', true ); - define( 'LLMS_REMOVE_ALL_DATA', true ); - } - - include( dirname( __FILE__, 4 ) . '/uninstall.php' ); - - wp_cache_flush(); - - LLMS_Install::install(); - $this->assertEquals( LLMS()->version, get_option( 'lifterlms_current_version' ) ); - - } - - /** - * Test get_can_install_user_id() method - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_can_install_user_id() { - - // Clean user* tables. - global $wpdb; - $wpdb->query( "TRUNCATE TABLE $wpdb->users" ); - $wpdb->query( "TRUNCATE TABLE $wpdb->usermeta" ); - - // No users, expect 0. - $this->assertEquals( 0, LLMS_Install::get_can_install_user_id() ); - - // Create a subscriber. - $subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) ); - - // No admin users, expect 0. - $this->assertEquals( 0, LLMS_Install::get_can_install_user_id() ); - - // Create two admins. - $admins = $this->factory->user->create_many( 2, array( 'role' => 'administrator' ) ); - - // Expect the first admin to be returned. - $this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() ); - - // Log in as subscriber. - wp_set_current_user( $subscriber ); - - // Expect the first admin to be returned. - $this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() ); - - // Log in as first admin. - wp_set_current_user( $admins[0] ); - - // Expect the first admin to be returned. - $this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() ); - - // Log in as second admin. - wp_set_current_user( $admins[1] ); - - // Expect the second admin to be returned. - $this->assertEquals( $admins[1], LLMS_Install::get_can_install_user_id() ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-integrations.php b/tests/phpunit/unit-tests/class-llms-test-integrations.php deleted file mode 100644 index 2fa5568c77..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-integrations.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Tests for the LLMS_Integrations class - * @group integrations - * @since 3.19.0 - * @version 3.19.0 - */ -class LLMS_Test_Integrations extends LLMS_UnitTestCase { - - /** - * test instance() method - * @return void - * @since 3.19.0 - * @version 3.19.0 - */ - public function test_instance() { - - $this->assertTrue( is_a( LLMS()->integrations(), 'LLMS_Integrations' ) ); - - } - - // public function test_get_integration() {} - - /** - * test init() method - * @return void - * @since 3.19.0 - * @version 3.19.0 - */ - public function test_init() { - - $instance = LLMS()->integrations(); - $this->assertEquals( 2, count( $instance->integrations() ) ); - - } - - /** - * Test get available integrations - * @return void - * @since 3.19.0 - * @version 3.19.0 - */ - public function test_get_available_integrations() { - - $instance = LLMS()->integrations(); - $this->assertEquals( array(), $instance->get_available_integrations() ); - - // enable an integration - update_option( 'llms_integration_bbpress_enabled', 'yes' ); - - // option is enabled but it's not available b/c deps. don't exist - $this->assertEquals( 0, count( $instance->get_available_integrations() ) ); - - // deps exist now - $stub = $this->getMockBuilder( 'bbPress' )->getMock(); - $this->assertEquals( 1, count( $instance->get_available_integrations() ) ); - - } - - /** - * Test integrations() method - * @return [type] - * @since 3.19.0 - * @version 3.19.0 - */ - public function test_integrations() { - - $instance = LLMS()->integrations(); - $this->assertEquals( 2, count( $instance->integrations() ) ); - - } - - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-llms-dom-document.php b/tests/phpunit/unit-tests/class-llms-test-llms-dom-document.php deleted file mode 100644 index 2fdd316642..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-llms-dom-document.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * Test LLMS_DOM_Document - * - * @package LifterLMS/Tests - * - * @group llms_dom_document - * - * @since 4.13.0 - */ -class LLMS_Test_LLMS_DOM_Document extends LLMS_Unit_Test_Case { - - /** - * Test DOMDocument library missing - * - * @since 4.13.0 - * - * @return void - */ - public function test_dom_document_missing_error() { - $llms_dom = new LLMS_DOM_Document( 'some string to load' ); - - // Simulate that the DOMDocument library is not available. - LLMS_Unit_Test_Util::set_private_property( - $llms_dom, - 'error', - new WP_Error( 'llms-dom-document-missing', __( 'DOMDocument not available.', 'lifterlms' ) ) - ); - $load = $llms_dom->load(); - - $this->assertWPError( $load ); - $this->assertWPErrorCodeEquals( 'llms-dom-document-missing', $load ); - } - - /** - * Test loading string success - * - * @since 4.13.0 - * - * @return void - */ - public function test_loading_success() { - - $llms_dom = new LLMS_DOM_Document( 'some string to load' ); - $this->assertTrue( $llms_dom->load() ); - } - - /** - * Test loading method switch - * - * @since 4.13.0 - * - * @return void - */ - public function test_loading_method_switch() { - - // Check that by default the loading method is 'load_with_mb_convert_encoding'. - $llms_dom = new LLMS_DOM_Document( 'some string to load' ); - - $load_method = LLMS_Unit_Test_Util::get_private_property_value( - $llms_dom, - 'load_method' - ); - - $this->assertEquals( 'load_with_mb_convert_encoding', $load_method ); - - // Force using utf fixer. - add_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false', 999 ); - - $llms_dom = new LLMS_DOM_Document( 'some other string to load' ); - - $load_method = LLMS_Unit_Test_Util::get_private_property_value( - $llms_dom, - 'load_method' - ); - - remove_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false', 999 ); - - $this->assertEquals( 'load_with_meta_utf_fixer', $load_method ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-main-class.php b/tests/phpunit/unit-tests/class-llms-test-main-class.php deleted file mode 100644 index c954a17035..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-main-class.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php -/** - * Tests for LifterLMS Main Class - * - * @package LifterLMS/Tests - * - * @group main_class - * - * @since 3.3.1 - * @since 3.21.1 Add localization tests. - * @since 4.0.0 Add tests for `init_session()` method. - * Remove tests against removed LLMS_SVG_DIR constant. - * @since 4.4.0 Add tests for `init_assets()` method. - */ -class LLMS_Test_Main_Class extends LLMS_UnitTestCase { - - /** - * Setup function - * - * @since 3.3.1 - * @since 5.3.3 Use `llms()` in favor of `LLMS()` and renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->llms = llms(); - } - - /** - * Test the `instance` property. - * - * @since 3.3.1 - * @since 5.3.0 Rename `_instance` property to `instance`. - * - * @return void - */ - public function test_llms_instance() { - - $this->assertClassHasStaticAttribute( 'instance', 'LifterLMS' ); - - } - - /** - * Test class constants - * - * @since 3.3.1 - * @since 4.0.0 Remove tests against removed LLMS_SVG_DIR constant. - * - * @return void - */ - public function test_constants() { - - $this->assertEquals( $this->llms->version, LLMS_VERSION ); - $this->assertNotEquals( LLMS_LOG_DIR, '' ); - $this->assertNotEquals( LLMS_PLUGIN_DIR, '' ); - $this->assertNotEquals( LLMS_PLUGIN_FILE, '' ); - $this->assertNotEquals( LLMS_TEMPLATE_PATH, '' ); - - } - - /** - * Test main instances - * - * @since 3.3.1 - * @since 5.8.0 Added tests for additional instances. - * - * @return void - */ - public function test_instances() { - - $tests = array( - array( 'LLMS_Achievements', 'achievements' ), - array( 'LLMS_Block_Templates', 'block_templates' ), - array( 'LLMS_Certificates', 'certificates' ), - array( 'LLMS_Engagements', 'engagements' ), - array( 'LLMS_Events', 'events' ), - array( 'LLMS_Grades', 'grades' ), - array( 'LLMS_Integrations', 'integrations' ), - array( 'LLMS_Emails', 'mailer' ), - array( 'LLMS_Notifications', 'notifications' ), - array( 'LLMS_Payment_Gateways', 'payment_gateways' ), - array( 'LLMS_Processors', 'processors' ), - ); - - foreach ( $tests as $test ) { - - list( $expected_class, $func ) = $test; - $this->assertInstanceOf( $expected_class, $this->llms->$func() ); - - } - - } - - /** - * Test the init_assets() method. - * - * @since 4.4.0 - * - * @return void - */ - public function test_init_assets() { - - $assets = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' ); - - $this->assertEquals( $assets, llms()->assets ); - - $this->assertEquals( require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-scripts.php', LLMS_Unit_Test_Util::get_private_property_value( $assets, 'scripts' ) ); - $this->assertEquals( require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-styles.php', LLMS_Unit_Test_Util::get_private_property_value( $assets, 'styles' ) ); - - } - - /** - * Test the init_session() method - * - * @since 4.0.0 - * - * @return void - */ - public function test_init_session() { - - // Clear the session. - LLMS()->session = null; - - // Initializes a new session. - $session = LLMS()->init_session(); - $this->assertTrue( is_a( $session, 'LLMS_Session' ) ); - $session->set( 'test', 'mock' ); - - // Call it again, should respond with the same session as before. - $this->assertEquals( $session->get_id(), LLMS()->init_session()->get_id() ); - $this->assertEquals( 'mock', LLMS()->init_session()->get( 'test' ) ); - - } - - /** - * Test plugin localization - * - * @since 3.21.1 - * @since 4.9.0 Improve tests. - * - * @return void - */ - public function test_localize() { - - $dirs = array( - WP_LANG_DIR . '/lifterlms', // "Safe" directory. - WP_LANG_DIR . '/plugins', // Default language directory. - WP_PLUGIN_DIR . '/lifterlms/languages', // Plugin language directory. - ); - - foreach ( $dirs as $dir ) { - - // Make sure the initial strings work. - $this->assertEquals( 'LifterLMS', __( 'LifterLMS', 'lifterlms' ), $dir ); - $this->assertEquals( 'Course', __( 'Course', 'lifterlms' ), $dir ); - - // Load a language file. - $file = LLMS_Unit_Test_Files::copy_asset( 'lifterlms-en_US.mo', $dir ); - $this->llms->localize(); - - $this->assertEquals( 'BetterLMS', __( 'LifterLMS', 'lifterlms' ), $dir ); - $this->assertEquals( 'Module', __( 'Module', 'lifterlms' ), $dir ); - - // Clean up. - LLMS_Unit_Test_Files::remove( $file ); - unload_textdomain( 'lifterlms' ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-mime-type-extractor.php b/tests/phpunit/unit-tests/class-llms-test-mime-type-extractor.php deleted file mode 100644 index 7e72e38b49..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-mime-type-extractor.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Test LLMS_Mime_Type_Extractor - * - * @package LifterLMS/Tests - * - * @group mime_type_extractor - * - * @since 3.38.1 - * @version 3.38.1 - */ -class LLMS_Test_Mime_Type_Extractor extends LLMS_UnitTestCase { - - /** - * Test files. - * - * @var array - */ - protected $files = array( - 'po' => 'lifterlms-en_US.po', - 'jpg' => 'christian-fregnan-unsplash.jpg', - ); - - /** - * Test from_file_path() for a file with a mime-type that exists - * - * @since 3.38.1 - * - * @return void - */ - public function test_mime_type_in_list() { - - global $lifterlms_tests; - $this->assertEquals( - 'image/jpeg', - LLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['jpg'] ) - ); - - } - - /** - * Test from_file_path() for a mime-type not found in our list - * - * @since 3.38.1 - * - * @return void - */ - public function test_mime_type_not_in_list() { - - global $lifterlms_tests; - - // I expect po files to be recognized by one of the fallback functions as 'text/plain' or 'text/x-po'. - if ( function_exists( 'finfo_file' ) || function_exists( 'mime_content_type' ) ) { - $this->assertContains( - LLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['po'] ), - array( - 'text/plain', - 'text/x-po' - ) - ); - } else { - $this->assertEquals( - LLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE, - LLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['po'] ) - ); - } - - - } - - /** - * Test from_file_path() for a file that does not exist - * - * @since 3.38.1 - * - * @return void - */ - public function test_mime_type_not_existent() { - - global $lifterlms_tests; - $this->assertEquals( - LLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE, - LLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . 'SomeoneJoinsSomethingTheyDoNotBelongTo.jpg' ) - ); - - } - - /** - * Test from_file_path() when checking a directory - * - * @since 3.38.1 - * - * @return void - */ - public function test_mime_type_of_a_dir() { - - global $lifterlms_tests; - $this->assertEquals( - LLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE, - LLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir ) - ); - - } -} diff --git a/tests/phpunit/unit-tests/class-llms-test-payment-gateway-integrations.php b/tests/phpunit/unit-tests/class-llms-test-payment-gateway-integrations.php deleted file mode 100644 index 57bc0c03c6..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-payment-gateway-integrations.php +++ /dev/null @@ -1,609 +0,0 @@ -<?php -/** - * Run round-trip payment tests using the mock testing gateway. - * - * @group payments - * - * @since 3.37.6 - * @since 3.37.12 Added additional assertion message information to assist in debug chaos-related failures. - * @since 3.37.14 Reduce number of tests run for monthly and yearly chaotic simulations. - * @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()`. - * @since 5.3.1 Declare the `$gateway` property. - */ -class LLMS_Test_Payment_Gateway_Integrations extends LLMS_UnitTestCase { - - /** - * @var LLMS_Payment_Gateway|false - */ - protected $gateway; - - /** - * Before the class runs, register the mock gateway. - * - * @since 3.37.6 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()` and renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - add_filter( 'lifterlms_payment_gateways', array( __CLASS__, 'add_mock_gateway' ) ); - - // We shouldn't be able to do this but currently we can so whatever. - llms()->payment_gateways()->__construct(); - - } - - /** - * After the class runs, remove the mock gateway. - * - * @since 3.37.6 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()` and renamed from `tearDownAfterClass()` for compat with WP core changes. - * - * @return void - */ - public static function tear_down_after_class() { - - remove_filter( 'lifterlms_payment_gateways', array( __CLASS__, 'add_mock_gateway' ) ); - - // The gateways class is a bit messed up and loads gateways weird. - // we need to remove the gateway manually so other tests don't break. - foreach ( llms()->payment_gateways()->payment_gateways as $i => $gateway ) { - if ( 'mock' === $gateway->id ) { - unset( llms()->payment_gateways()->payment_gateways[ $i ] ); - } - } - parent::tear_down_after_class(); - - } - - /** - * Setup the test case. - * - * @since 3.37.6 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->gateway = llms()->payment_gateways()->get_gateway_by_id( 'mock' ); - } - - /** - * Register mock gateway - * - * @since 3.37.6 - * - * @param string[] $gateways Array of gateway class names - * - * @return string[] - */ - public static function add_mock_gateway( $gateways ) { - $gateways[] = 'LLMS_Payment_Gateway_Mock'; - return $gateways; - } - - /** - * Sets up a mock order for use with tests. - * - * @since 3.37.6 - * - * @param string $period Access plan period value. - * @param int $frequency Access plan frequency value. - * @return LLMS_Order - */ - private function setup_order( $period, $frequency = 1 ) { - - // Setup the objects. - $student = $this->factory->student->create_and_get(); - - $plan = $this->get_mock_plan(); - $plan->set( 'period', $period ); - $plan->set( 'frequency', $frequency ); - - $order = new LLMS_Order( 'new' ); - $order = $order->init( $student, $plan, $this->gateway ); - - // Process the order. - $this->gateway->handle_pending_order( $order, $plan, $student ); - - return $order; - - } - - /** - * Run some tests on the initial setup of the order and the first payment. - * - * @since 3.37.6 - * @since 5.3.3 Use assertEqualsWithDelta() in favor of 4th parameter supplied to assertEquals(). - * - * @param LLMS_Order $order The order. - * @return void - */ - private function do_order_setup_tests( $order ) { - - $plan = llms_get_post( $order->get( 'plan_id' ) ); - $period = $plan->get( 'period' ); - $frequency = $plan->get( 'frequency' ); - - // Order should be active. - $this->assertEquals( 'llms-active', $order->get( 'status' ) ); - - // Check there's only 1 transaction. - $txns = $order->get_transactions(); - $this->assertEquals( 1, $txns['count'] ); - - // Transaction succeeded. - $last = array_pop( $txns['transactions'] ); - $this->assertEquals( 'llms-txn-succeeded', $last->get( 'status' ) ); - - // Next payment date. - $next_payment_time = $order->get_date( 'date_next_payment', 'U' ); - $this->assertEqualsWithDelta( strtotime( "+{$frequency} {$period}", $order->get_date( 'date', 'U' ) ), $next_payment_time, 5, $period ); // 5 seconds tolerance. - - } - - /** - * Runs N charges on a recurring order with optionally included "chaos". - * - * "Chaos" will run the recurring payment randomly between $chaos_hours before and $chaos_hours after the scheduled payment time. - * - * @since 3.37.6 - * @since 3.37.12 Added additional assertion message information to assist in debug chaos-related failures. - * @since 5.3.1 If the chaos >= 0, calculate the expected next payment time based on the scheduled payment time. - * @since 5.3.3 Use assertEqualsWithDelta() in favor of 4th parameter supplied to assertEquals(). - * - * @param LLMS_Order $order Initialized order to run charges against. - * @param int $num Number of charges to run. - * @param int $chaos_hours Number of hours of chaos to introduce. - * @param int $delta_hours Number of hours of tolerance to allow as the "delta" for date comparison assertions. - * @return void - */ - private function do_n_charges_for_order( $order, $num, $chaos_hours = 0, $delta_hours = 0 ) { - - $plan = llms_get_post( $order->get( 'plan_id' ) ); - $period = $plan->get( 'period' ); - $frequency = $plan->get( 'frequency' ); - - $start = microtime( true ); - $limit = 2.5; - $elapsed = 0; - $i = 2; - while ( $i <= $num + 1 && $elapsed <= $limit ) { - - $scheduled_payment_time = (int) $order->get_date( 'date_next_payment', 'U' ); - - // Run the recurring payment randomly between 12 hours before and 12 hours after the scheduled payment time. - $chaos = rand( 0, HOUR_IN_SECONDS * $chaos_hours ) * ( rand( 0, 1 ) ? -1 : 1 ); - - // Time travel. - llms_tests_mock_current_time( $scheduled_payment_time + $chaos ); - - // Run the transaction. - $this->gateway->handle_recurring_transaction( $order ); - - $txns = $order->get_transactions(); - $last_txn = array_shift( $txns['transactions'] ); - $last_txn_time = $last_txn->get_date( 'date', 'U' ); - - // Should have transactions equal to the current loop interval. - $this->assertEquals( $i, $txns['total'] ); - - // Last transaction date should equal the chaos time, this way we can be sure it was the payment we thought it was. - $this->assertEquals( $last_txn->get_date( 'date', 'U' ), $scheduled_payment_time + $chaos ); - - $next_payment_time = $order->get_date( 'date_next_payment', 'U' ); - - if ( $chaos < 0 ) { - $expect = strtotime( "+{$frequency} {$period}", $last_txn_time ); - } else { - $expect = strtotime( "+{$frequency} {$period}", $scheduled_payment_time ); - } - $msg = sprintf( - '%1$s Payment #%2$d: Got %3$s and expected %4$s ( $chaos_hours = %5$d | $chaos = %6$s )', - ucfirst( $period ), - $i, - date( 'Y-m-d H:i:s', $next_payment_time ), - date( 'Y-m-d H:i:s', $expect ), - $chaos_hours, - $chaos - ); - - // Ensure that the calculated next payment time is 1 period +/- 23:59:59 from the previous transaction. - $this->assertEqualsWithDelta( $expect, $next_payment_time, $delta_hours ? $delta_hours * HOUR_IN_SECONDS - 1 : 0, $msg ); - - ++$i; - $elapsed = microtime( true ) - $start; - - } - - // if ( $elapsed > $limit ) { - - // $trace = debug_backtrace(); - // $caller = $trace[1]; - - // $this->markTestSkipped( "{$caller['class']}::{$caller['function']}: {$i}" ); - - // } - - } - - /** - * Run tests for a for a daily plan - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_day_plan() { - - $order = $this->setup_order( 'day' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99 ); - - } - - /** - * Run tests for a for a daily plan with irregular frequency - * - * @since 3.37.6 - * - * @return void - */ - public function test_recurring_lifecycle_for_day_plan_with_frequency() { - - $order = $this->setup_order( 'day', 3 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 10 ); - - } - - /** - * Run tests for a for a daily plan_with_chaos - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_day_plan_with_chaos() { - - $order = $this->setup_order( 'day' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99, 6, 12 ); - - } - - /** - * Run tests for a for a daily plan with chaos and irregular frequency - * - * @since 3.37.6 - * - * @return void - */ - public function test_recurring_lifecycle_for_day_plan_with_chaos_and_frequency() { - - $order = $this->setup_order( 'day', 3 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 25, 6, 12 ); - - } - - /** - * Run tests for a for a weekly plan - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_week_plan() { - - $order = $this->setup_order( 'week' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99 ); - - } - - /** - * Run tests for a for a weekly plan with irregular frequency - * - * @since 3.37.6 - * - * @return void - */ - public function test_recurring_lifecycle_for_week_plan_with_frequency() { - - $order = $this->setup_order( 'week', 8 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 10 ); - - } - - /** - * Run tests for a for a weekly plan_with_chaos - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_week_plan_with_chaos() { - - $order = $this->setup_order( 'week' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99, 12, 24 ); - - } - - /** - * Run tests for a for a weekly plan with chaos and irregular frequency - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_week_plan_with_chaos_and_frequency() { - - $order = $this->setup_order( 'week', 2 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99, 12, 24 ); - - } - - /** - * Run tests for a for a monthly plan - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_month_plan() { - - $order = $this->setup_order( 'month' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99 ); - - } - - /** - * Run tests for a for a monthly plan with irregular frequency - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_month_plan_with_frequency() { - - $order = $this->setup_order( 'month', 2 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99 ); - - } - - /** - * Run tests for a for a monthly plan_with_chaos - * - * @since 3.37.6 - * @since 3.37.14 Reduce number of tests run. - * @since 4.3.1 Increased delta from 24 to 48 hours. - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_month_plan_with_chaos() { - - $order = $this->setup_order( 'month' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 50, 12, 48 ); - - } - - /** - * Run tests for a for a monthly plan with chaos and irregular frequency - * - * @since 3.37.6 - * @since 4.3.1 Increased delta from 24 to 48 hours. - * - * @return void - */ - public function test_recurring_lifecycle_for_month_plan_with_chaos_and_frequency() { - - $order = $this->setup_order( 'month', 3 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 31, 12, 48 ); - - } - - /** - * Run tests for a for a yearly plan - * - * @since 3.37.6 - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_year_plan() { - - $order = $this->setup_order( 'year' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 99 ); - - } - - /** - * Run tests for a for a yearly plan with irregular frequency - * - * @since 3.37.6 - * - * @return void - */ - public function test_recurring_lifecycle_for_year_plan_with_frequency() { - - $order = $this->setup_order( 'year', 5 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 20 ); - - } - - /** - * Run tests for a for a yearly plan_with_chaos - * - * @since 3.37.6 - * @since 3.37.14 Reduce number of tests run. - * - * @medium - * - * @return void - */ - public function test_recurring_lifecycle_for_year_plan_with_chaos() { - - $order = $this->setup_order( 'year' ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 50, 12, 24 ); - - } - - /** - * Run tests for a for a yearly plan with chaos and irregular frequency - * - * @since 3.37.6 - * - * @return void - */ - public function test_recurring_lifecycle_for_year_plan_with_chaos_and_frequency() { - - $order = $this->setup_order( 'year', 2 ); - - // Reinitialize the order for assertions. - $order = llms_get_post( $order->get( 'id' ) ); - - // Test setup data. - $this->do_order_setup_tests( $order ); - - // Run recurring charges for the order. - $this->do_n_charges_for_order( $order, 9, 12, 24 ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-payment-gateways.php b/tests/phpunit/unit-tests/class-llms-test-payment-gateways.php deleted file mode 100644 index 5e69cb3713..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-payment-gateways.php +++ /dev/null @@ -1,135 +0,0 @@ -<?php -/** - * Tests for the LLMS_Payment_Gateways class - * - * @group payment_gateways - * - * @since 3.10.0 - */ -class LLMS_Test_Payment_Gateways extends LLMS_UnitTestCase { - - /** - * Enable or disable a payment gateway by ID - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @param string $id Gateway id. - * @param string $enabled Whether the gateway should be enabled or disabled. Accepts on or off. - * @return void - */ - private function toggle_gateway( $id, $enabled = 'on' ) { - - $enabled = 'on' === $enabled ? 'yes' : 'no'; - - $manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $manual->get_option_name( 'enabled' ), $enabled ); - - } - - /** - * Test get_enabled_payment_gateways function - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @return void - */ - public function test_get_enabled_payment_gateways() { - - $gways = llms()->payment_gateways(); - - $this->toggle_gateway( 'manual', 'off' ); - - $this->assertEquals( array(), $gways->get_enabled_payment_gateways() ); - - // enable the manual gateway - $this->toggle_gateway( 'manual', 'on' ); - - // gateway should exist in the array - $this->assertTrue( is_array( $gways->get_enabled_payment_gateways() ) ); - $this->assertTrue( array_key_exists( 'manual', $gways->get_enabled_payment_gateways() ) ); - $this->assertEquals( 1, count( $gways->get_enabled_payment_gateways() ) ); - - } - - /** - * Test get_default_gateway() function - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @return void - */ - public function test_get_default_gateway() { - - // enable the manual gateway - $this->toggle_gateway( 'manual', 'on' ); - $this->assertEquals( 'manual', llms()->payment_gateways()->get_default_gateway() ); - - } - - /** - * Test get_payment_gateways() method - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @return void - */ - public function test_get_payment_gateways() { - - $gways = llms()->payment_gateways(); - - $this->assertTrue( is_array( $gways->get_payment_gateways() ) ); - $this->assertTrue( array_key_exists( 'manual', $gways->get_payment_gateways() ) ); - $this->assertEquals( 1, count( $gways->get_payment_gateways() ) ); - - } - - /** - * Test has_gateways() method - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @return void - */ - public function test_has_gateways() { - - $gways = llms()->payment_gateways(); - - // check all gateways (default) - $this->assertTrue( $gways->has_gateways() ); - // check all gateways passing false - $this->assertTrue( $gways->has_gateways( false ) ); - - // check enabled - $this->toggle_gateway( 'manual', 'off' ); - $this->assertFalse( $gways->has_gateways( true ) ); - - $this->toggle_gateway( 'manual', 'on' ); - $this->assertTrue( $gways->has_gateways( true ) ); - - } - - /** - * Test get_gateway_by_id() - * - * @since 3.10.0 - * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`. - * - * @return void - */ - public function test_get_gateway_by_id() { - - $gways = llms()->payment_gateways(); - $manual = $gways->get_gateway_by_id( 'manual' ); - $this->assertTrue( is_a( $manual, 'LLMS_Payment_Gateway' ) ); - $this->assertEquals( 'manual', $manual->get_id() ); - - $this->assertFalse( $gways->get_gateway_by_id( 'fake_gway' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-playnice.php b/tests/phpunit/unit-tests/class-llms-test-playnice.php deleted file mode 100644 index cf51a71c34..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-playnice.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Tests for the LLMS_PlayNice Class - * @since 3.19.6 - * @version 3.19.6 - */ -class LLMS_Test_PlayNice extends LLMS_UnitTestCase { - - /** - * Tests for wp_optimizepress_live_editor() - * @return void - * @since 3.19.6 - * @version 3.19.6 - */ - public function test_wp_optimizepress_live_editor() { - - $play = new LLMS_PlayNice(); - $this->assertNull( $play->wp_optimizepress_live_editor() ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-post-instructors.php b/tests/phpunit/unit-tests/class-llms-test-post-instructors.php deleted file mode 100644 index 4a134e9fdc..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-post-instructors.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php -/** - * Tests for LLMS_Post_Instructors model & functions - * - * @package LifterLMS/Tests - * - * @group LLMS_Post_Instructors - * @group LLMS_Course - * @group LLMS_Membership - * - * @since 3.13.0 - */ -class LLMS_Test_Post_Instructors extends LLMS_UnitTestCase { - - private $post_types = array( 'course', 'llms_membership' ); - - public function test_interface() { - - foreach ( $this->post_types as $post_type ) { - - $post_id = $this->factory->post->create( array( - 'post_type' => $post_type, - ) ); - - $post = llms_get_post( $post_id ); - - $this->assertTrue( method_exists( $post, 'instructors' ) ); - $this->assertTrue( method_exists( $post, 'get_instructors' ) ); - $this->assertTrue( method_exists( $post, 'set_instructors' ) ); - - $this->assertTrue( is_a( $post->instructors(), 'LLMS_Post_Instructors' ) ); - - } - - } - - /** - * Test get and set methods. - * - * @since Unknown - * @since 4.2.0 Added check to ensure `name` is set when no instructor data is set. - * - * @return void - */ - public function test_getters_setters() { - - $user_ids = $this->factory->user->create_many( 3 ); - - foreach ( $this->post_types as $post_type ) { - - $post_id = $this->factory->post->create( array( - 'post_type' => $post_type, - 'post_author' => $user_ids[0], - ) ); - - $post = llms_get_post( $post_id ); - - $defaults = llms_get_instructors_defaults(); - - $this->assertTrue( is_array( $post->get_instructors() ) ); - - $post->set_instructors( array( - array( 'id' => $user_ids[0] ), - array( 'id' => $user_ids[1] ), - array( 'id' => $user_ids[2] ), - ) ); - - foreach ( $post->get_instructors() as $instructor ) { - - $this->assertTrue( in_array( $instructor['id'], $user_ids ) ); - $this->assertEquals( $defaults['label'], $instructor['label'] ); - $this->assertEquals( $defaults['visibility'], $instructor['visibility'] ); - - } - - $this->assertEquals( $post->get( 'author' ), $user_ids[0] ); - - $update = array( - array( - 'id' => $user_ids[1], - 'label' => 'mock label', - 'visibility' => 'visible', - ), - array( - 'id' => $user_ids[0], - 'label' => 'mock label', - 'visibility' => 'hidden', - ), - ); - $post->set_instructors( $update ); - $this->assertEquals( $update, $post->get_instructors() ); - - // Check exclude hidden works right. - unset( $update[1] ); - $this->assertEquals( $update, $post->get_instructors( true ) ); - - - // Clear instructors, should respond with a default of the post_author. - $post->set_instructors(); - $expect = $defaults; - $expect['id'] = $user_ids[1]; - $author = get_userdata( $user_ids[1] ); - $expect['name'] = $author->display_name; - $this->assertEquals( array( $expect ), $post->get_instructors() ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-post-relationships.php b/tests/phpunit/unit-tests/class-llms-test-post-relationships.php deleted file mode 100644 index 8e6a29b672..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-post-relationships.php +++ /dev/null @@ -1,398 +0,0 @@ -<?php -/** - * Tests for LLMS_Post_Instructors model & functions - * - * @group post_relationships - * - * @since 3.16.12 - * @since 3.37.8 Added tests to remove quiz attempts upon quiz deletion. - * @since 4.15.0 Added tests on access plans deletion upon quiz deletion. - * @since 5.4.0 Added tests for static methods delete_product_with_active_subscriptions_error_message() and maybe_prevent_product_deletion(). - */ -class LLMS_Test_Post_Relationships extends LLMS_UnitTestCase { - - /** - * When deleting lessons - * - * A) Any lesson which has this lesson as a prereq should have that prereq removed - * And the has_prereq metavalue should be unset returning "no" - * B) Any quiz attached to this lesson should be detached (making it an orphan) - * - * @since 3.16.12 - * @return void - */ - private function delete_lesson() { - - $courses = $this->generate_mock_courses( 1, 1, 4, 3, 1 ); - $lessons = llms_get_post( $courses[0] )->get_lessons(); - - // add prereqs to all the lessons except the first. - foreach ( $lessons as $i => $lesson ) { - - if ( 0 === $i ) { - continue; - } - - $prev = $lessons[ $i - 1 ]; - - $lesson->set( 'has_prerequisite', 'yes' ); - $lesson->set( 'prerequisite', $prev->get( 'id' ) ); - - } - - // Delete posts and run tests. - foreach ( $lessons as $i => $lesson ) { - - $quiz = $lesson->get_quiz(); - - wp_delete_post( $lesson->get( 'id' ) ); - - // Quizzes attached to the lesson should now be orphaned. - if ( $quiz ) { - $this->assertTrue( $quiz->is_orphan() ); - } - - if ( $i === count( $lessons ) - 1 ) { - continue; - } - $next = $lessons[ $i + 1 ]; - - // Prereqs should be removed. - $this->assertEquals( 'no', $next->get( 'has_prerequisite' ) ); - $this->assertEquals( 0, $next->get( 'prerequisite' ) ); - $this->assertFalse( $next->has_prerequisite() ); - - } - - } - - /** - * When a quiz is deleted, all the child questions should be deleted too - * - * Lesson should switch quiz_enabled to "no". - * - * All student attempts for the quiz should be deleted. - * - * @since 3.16.12 - * @since 3.37.8 Add tests to remove quiz attempts upon quiz deletion. - * - * @return void - */ - private function delete_quiz() { - - $courses = $this->generate_mock_courses( 1, 1, 1, 1, 20 ); - $lesson = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] ); - $quiz = $lesson->get_quiz(); - $quiz_id = $quiz->get( 'id' ); - - $student_1 = $this->factory->student->create(); - $attempt_1 = $this->take_quiz( $quiz_id, $student_1 ); - $student_2 = $this->factory->student->create(); - $attempt_2 = $this->take_quiz( $quiz_id, $student_2, 50 ); - - $questions = $quiz->get_questions( 'ids' ); - - wp_delete_post( $quiz->get( 'id' ), true ); - - // All question posts should be deleted. - foreach ( $questions as $question_id ) { - $this->assertNull( get_post( $question_id ) ); - } - - // The quiz will be disabled on the lesson because metadata is unset. - $this->assertFalse( $lesson->is_quiz_enabled() ); - - // Quiz attempts should be deleted. - $this->assertFalse( $attempt_1->exists() ); - $this->assertFalse( $attempt_2->exists() ); - - // Query for quiz attempts should return nothing. - $query = new LLMS_Query_Quiz_Attempt( - array( - 'quiz_id' => $quiz_id, - 'per_page' => 1, - ) - ); - $this->assertEquals( 0, $query->found_results ); - - } - - /** - * When a product is deleted all the related access plans should be deleted - * - * @since 4.15.0 - * - * @return void - */ - private function delete_product() { - - $product_types = array( - 'course', - 'llms_membership', - ); - - foreach ( $product_types as $product_type ) { - - // Create product. - $product_id = $this->factory->post->create( array( 'post_type' => $product_type ) ); - $title = sprintf( 'Access plan for %1$s', $product_id ); - - // Create access plan and assign the related product to it. - $access_plan = llms_insert_access_plan( compact( 'product_id', 'title' ) ); - $access_plan_id = $access_plan->get( 'id' ); - - // Get access plan properties (meta) to test. - if ( ! isset( $access_plan_metas ) ) { - $access_plan_metas = array_map( - function( $prop ) use ( $access_plan ) { - return LLMS_Unit_Test_Util::get_private_property_value( $access_plan, 'meta_prefix' ) . $prop; - }, - array_keys( - array_diff_key( - $access_plan->get( 'properties' ), - LLMS_Unit_Test_Util::call_method( $access_plan, 'get_post_properties' ) - ) - ) - ); - } - // Trash product => do not remove access plans. - wp_trash_post( $product_id ); - $this->assertNotNull( get_post( $product_id ), $product_type ); - - // Delete the product (no trash, force deletion is true by default for non built-in post types). - wp_delete_post( $product_id ); - - // Check the access plan has been deleted. - $this->assertNull( get_post( $product_id ), $product_type ); - - // Check access plan's meta deletion - foreach ( $access_plan_metas as $access_plan_meta ) { - $this->assertFalse( - metadata_exists( 'post', $access_plan_id, $access_plan_meta ), - sprintf( - 'Test failing for meta %1$s of access plan with ID %2$s on %3$s deletion', - $access_plan_meta, - $access_plan_id, - $product_type - ) - ); - } - - } - - } - - /** - * Test all relationships based on post types - * - * @since 3.16.12 - * @since 4.15.0 Added tests on course on membership deletion. - * - * @return void - */ - public function test_maybe_update_relationships() { - - $funcs = array( - 'delete_quiz', - 'delete_lesson', - 'delete_product', - ); - foreach ( $funcs as $func ) { - $this->{$func}(); - } - - } - - /** - * Test delete_product_with_active_subscriptions_error_message(). - * - * @since 5.4.0 - * - * @return void - */ - public function test_delete_product_with_active_subscriptions_error_message() { - - $post_types = array( - 'post', - 'course', - 'llms_membership', - ); - - foreach ( $post_types as $post_type ) { - - // Create post/product. - $post_id = $this->factory->post->create( array( 'post_type' => $post_type ) ); - - $post_type_object = get_post_type_object( $post_type ); - $post_type_name = $post_type_object->labels->name; - - $this->assertEquals( - 'post' !== $post_type ? - sprintf( - 'Sorry, you are not allowed to delete %s with active subscriptions.', - $post_type_name - ): - '' - , - LLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id ) - ); - } - - } - - - /** - * Test maybe_prevent_product_deletion() - * - * @since 5.4.0 - * - * @return void - */ - public function test_maybe_prevent_product_deletion() { - - $post_types = array( - 'post', - 'course', - 'llms_membership', - ); - - foreach ( $post_types as $post_type ) { - - // Create post/product. - $post_id = $this->factory->post->create( array( 'post_type' => $post_type ) ); - - wp_delete_post( $post_id, true ); - - $this->assertEmpty( - get_post( $post_id ), - $post_type - ); - - } - - unset( $post_types[0] ); - - // Courses and Memberships are deletable if associated to a single-payment order. - foreach ( $post_types as $post_type ) { - - // Create product. - $post_id = $this->factory->post->create( array( 'post_type' => $post_type ) ); - - // Create an active subscription per product. - $order = $this->get_mock_order(); - $order->set( 'product_id', $post_id ); - $order->set( 'order_type', 'single' ); - - wp_delete_post( $post_id ); - - $this->assertEmpty( - get_post( $post_id ), - $post_type - ); - - } - - // Courses and Memberships are deletable if associated to a recurring payment order depending on whether there are active subscriptions. - foreach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) { - foreach ( $post_types as $post_type ) { - - // Create product. - $post_id = $this->factory->post->create( array( 'post_type' => $post_type ) ); - - // Create an active subscription per product. - $order = $this->get_mock_order(); - $order->set( 'product_id', $post_id ); - $order->set( 'order_type', 'recurring' ); - $order->set( 'status', $status ); - - $expected_error_message = LLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id ); - - try { - wp_delete_post( $post_id ); - } catch( WPDieException $e ) { - $this->assertEquals( - $expected_error_message, - $e->getMessage() - ); - } - // Test if subscription active no deletion occurred. - $_test = in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ? 'assertNotEmpty' : 'assertEmpty'; - $this->$_test( - get_post( $post_id ), - "{$post_type} : {$status}" - ); - - } - } - - } - - /** - * Test maybe_prevent_product_deletion() via REST API. - * - * @since 5.4.0 - * - * @return void - */ - public function test_maybe_prevent_product_deletion_rest_api() { - - $post_types = array( - 'course' => 'courses', - 'llms_membership' => 'memberships', - ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Force llms_is_rest. - add_filter( 'llms_is_rest', '__return_true' ); - - // Courses and Memberships are deletable if associated to a recurring payment order depending on whether there are active subscriptions. - foreach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) { - - foreach ( $post_types as $post_type => $endpoint ) { - - // Create product. - $post_id = $this->factory->post->create( array( 'post_type' => $post_type ) ); - - // Create an active subscription per product. - $order = $this->get_mock_order(); - - $order->set( 'product_id', $post_id ); - $order->set( 'order_type', 'recurring' ); - $order->set( 'status', $status ); - - $expected_error_message = LLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id ); - - $request = new WP_REST_Request( - 'DELETE', - "/llms/v1/{$endpoint}/{$post_id}" - ); - - $request->set_param( 'force', 'true' ); - $res = rest_get_server()->dispatch( $request ); - - if ( in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ) { - // Not deleted. - $this->assertNotEmpty( - get_post( $post_id ), - "{$post_type} : {$status}" - ); - - $this->assertEquals( 500, $res->get_status(), "{$post_type} : {$status}" ); - $this->assertEquals( $expected_error_message, $res->get_data()['message'], "{$post_type} : {$status}" ); - } else { - // Deleted. - $this->assertEmpty( - get_post( $post_id ), - "{$post_type} : {$status}" - ); - } - - } - - } - - remove_filter( 'llms_is_rest', '__return_true' ); - - } -} diff --git a/tests/phpunit/unit-tests/class-llms-test-post-types.php b/tests/phpunit/unit-tests/class-llms-test-post-types.php deleted file mode 100644 index e1696fcc05..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-post-types.php +++ /dev/null @@ -1,169 +0,0 @@ -<?php -/** - * Tests for LifterLMS Custom Post Types - * - * @group LLMS_Post_Types - * - * @since 3.13.0 - * @since 5.5.0 Addedd tests for deprecated filters of the type "lifterlms_register_post_type_${prefixed_post_type_name}". - */ -class LLMS_Test_Post_Types extends LLMS_UnitTestCase { - - public function test_deregister_sitemap_post_types() { - - $mock = array( - 'post' => true, - 'page' => true, - 'course' => true, - 'lesson' => true, - 'llms_quiz' => true, - 'llms_certificate' => true, - 'llms_my_certificate' => true - ); - - $expect = array( - 'post' => true, - 'page' => true, - 'course' => true, - ); - - $this->assertEquals( $expect, LLMS_Post_Types::deregister_sitemap_post_types( $mock ) ); - - } - - public function test_register_post_taxonomies() { - - LLMS_Post_Types::register_taxonomies(); - - $taxonomies = array( - 'course_cat', - 'course_difficulty', - 'course_tag', - 'course_track', - 'membership_cat', - 'membership_tag', - 'llms_product_visibility', - 'llms_access_plan_visibility', - ); - - foreach ( $taxonomies as $name ) { - // var_dump( sprintf( '%s: %s', $name, taxonomy_exists( $name ) ) ); - $this->assertTrue( taxonomy_exists( $name ) ); - } - - } - - public function test_register_post_types() { - - LLMS_Post_Types::register_post_types(); - - $post_types = array( - 'course', - 'section', - 'lesson', - 'llms_membership', - 'llms_engagement', - 'llms_order', - 'llms_transaction', - 'llms_achievement', - 'llms_certificate', - 'llms_my_certificate', - 'llms_email', - 'llms_quiz', - 'llms_question', - 'llms_coupon', - 'llms_voucher', - 'llms_review', - 'llms_access_plan', - ); - - foreach ( $post_types as $name ) { - $this->assertTrue( post_type_exists( $name ) ); - } - - } - - public function test_register_post_statuses() { - - LLMS_Post_Types::register_post_statuses(); - - $statuses = array( - 'llms-completed', - 'llms-active', - 'llms-expired', - 'llms-on-hold', - 'llms-pending', - 'llms-cancelled', - 'llms-refunded', - 'llms-failed', - 'llms-txn-failed', - 'llms-txn-pending', - 'llms-txn-refunded', - 'llms-txn-succeeded', - ); - - foreach ( $statuses as $name ) { - $this->assertTrue( ! is_null( get_post_status_object( $name ) ) ); - } - - } - - /** - * Test deprecated filters of the type "lifterlms_register_post_type_${prefixed_post_type_name}". - * - * @expectedDeprecated lifterlms_register_post_type_llms_membership - * @expectedDeprecated lifterlms_register_post_type_llms_engagement - * @expectedDeprecated lifterlms_register_post_type_llms_order - * @expectedDeprecated lifterlms_register_post_type_llms_transaction - * @expectedDeprecated lifterlms_register_post_type_llms_achievement - * @expectedDeprecated lifterlms_register_post_type_llms_certificate - * @expectedDeprecated lifterlms_register_post_type_llms_my_certificate - * @expectedDeprecated lifterlms_register_post_type_llms_email - * @expectedDeprecated lifterlms_register_post_type_llms_quiz - * @expectedDeprecated lifterlms_register_post_type_llms_question - * @expectedDeprecated lifterlms_register_post_type_llms_coupon - * @expectedDeprecated lifterlms_register_post_type_llms_voucher - * @expectedDeprecated lifterlms_register_post_type_llms_review - * @expectedDeprecated lifterlms_register_post_type_llms_access_plan - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @since 5.5.0 - * - * @return void - */ - public function test_deprecated_filters() { - - $post_types = array( - 'course', - 'section', - 'lesson', - 'llms_membership', - 'llms_engagement', - 'llms_order', - 'llms_transaction', - 'llms_achievement', - 'llms_certificate', - 'llms_my_certificate', - 'llms_email', - 'llms_quiz', - 'llms_question', - 'llms_coupon', - 'llms_voucher', - 'llms_review', - 'llms_access_plan', - ); - - foreach ( $post_types as $post_type ) { - - unregister_post_type( $post_type ); - add_filter( "lifterlms_register_post_type_${post_type}", '__return_empty_array' ); - LLMS_Post_Types::register_post_type( $post_type, array() ); - remove_filter( "lifterlms_register_post_type_${post_type}", '__return_empty_array' ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-prevent-concurrent-logins.php b/tests/phpunit/unit-tests/class-llms-test-prevent-concurrent-logins.php deleted file mode 100644 index 56e9c5bf24..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-prevent-concurrent-logins.php +++ /dev/null @@ -1,310 +0,0 @@ -<?php -/** - * Tests for LifterLMS Prevemt Concurrent Logins class - * - * @group LLMS_Prevent_Concurrent_Logins - * - * @since 5.6.0 - */ -class LLMS_Test_Prevent_Concurrent_Logins extends LLMS_UnitTestCase { - - /** - * Test maybe_prevent_concurrent_logins(). - * - * @since 5.6.0 - * - * @return void - */ - public function test_maybe_prevent_concurrent_logins() { - - // By default the 'student' role is not allowed to log-in multiple times at once. - $user_id = $this->factory->user->create( array( 'role' => 'student' ) ); - $session_1 = $this->_log_in( $user_id, time() + DAY_IN_SECONDS ); - $this->assertEquals( - array( - $session_1, - ), - wp_get_all_sessions() - ); - - // First login, nothing to prevent. - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - false, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - - // Another login. - $session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_1, - $session_2, - ), - wp_get_all_sessions() - ); - - // Second login, the first session should be destroyed. - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - true, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - $this->assertEquals( - array( $session_2 ), - wp_get_all_sessions() - ); - - } - - /** - * Test maybe_prevent_concurrent_logins(). - * - * @since 5.6.0 - * - * @return void - */ - public function test_maybe_prevent_concurrent_logins_allow_roles() { - $prevent_option = get_option( 'lifterlms_prevent_concurrent_logins' ); - $roles_option = get_option( 'lifterlms_prevent_concurrent_logins_roles' ); - - // By default the 'student' role is not allowed to log-in multiple times at once. - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); - $session_1 = $this->_log_in( $user_id, time() + DAY_IN_SECONDS ); - $this->assertEquals( - array( - $session_1, - ), - wp_get_all_sessions() - ); - - // First login, nothing to prevent. - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - false, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - - // Another login. - $session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_1, - $session_2, - ), - wp_get_all_sessions() - ); - - // Second login, since we're an allowed role, there's nothing to prevent. - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - false, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - $this->assertEquals( - array( - $session_1, - $session_2, - ), - wp_get_all_sessions() - ); - - // Change the allowed role option to disallow administrators. - update_option( 'lifterlms_prevent_concurrent_logins_roles', array( 'administrator' ) ); - - // Another login. - $session_3 = $this->_log_in( $user_id, time() + ( 3 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_1, - $session_2, - $session_3, - ), - wp_get_all_sessions() - ); - - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - true, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - $this->assertEquals( - array( - $session_3, - ), - wp_get_all_sessions() - ); - - // Allow current user to login mutiple times via a filter. - $allow_current_user = function( $allow, $uid ) use ( $user_id ) { - return $uid === $user_id ? true : $allow; - }; - add_filter( 'llms_allow_user_concurrent_logins', $allow_current_user, 10, 2 ); - - // Another login. - $session_4 = $this->_log_in( $user_id, time() + ( 4 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_3, - $session_4, - ), - wp_get_all_sessions() - ); - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - false, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - $this->assertEquals( - array( - $session_3, - $session_4, - ), - wp_get_all_sessions() - ); - - remove_filter( 'llms_allow_user_concurrent_logins', $allow_current_user, 10, 2 ); - - // Change the allowed role option to an empty array. - update_option( 'lifterlms_prevent_concurrent_logins_roles', array() ); - // Another login. - $session_5 = $this->_log_in( $user_id, time() + ( 5 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_3, - $session_4, - $session_5, - ), - wp_get_all_sessions() - ); - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - false, - LLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins() - ); - $this->assertEquals( - array( - $session_3, - $session_4, - $session_5, - ), - wp_get_all_sessions() - ); - - // Reset. - update_option( 'lifterlms_prevent_concurrent_logins', $prevent_option ); - update_option( 'lifterlms_prevent_concurrent_logins_roles', $roles_option ); - - } - - /** - * Test maybe_prevent_concurrent_logins(). - * - * @since 5.6.0 - * - * @return void - */ - public function test_destroy_all_sessions_but_newest() { - - // First login, it's also the newest, I expect it to be kept: 1. - $user_id = $this->factory->user->create(); - $session_1 = $this->_log_in( $user_id, time() + ( 1 * DAY_IN_SECONDS ), $first_token ); - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - 1, - LLMS_Unit_Test_Util::call_method( - LLMS_Prevent_Concurrent_Logins::instance(), - 'destroy_all_sessions_but_newest' - ) - ); - $this->assertEquals( - array( - $session_1 - ), - wp_get_all_sessions() - ); - - // Another login. - $session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) ); - $this->assertEquals( - array( - $session_1, - $session_2, - ), - wp_get_all_sessions() - ); - - // Second login, it's also the newest, I expect it to be kept: 1. - LLMS_Prevent_Concurrent_Logins::instance()->init(); - $this->assertEquals( - 1, - LLMS_Unit_Test_Util::call_method( - LLMS_Prevent_Concurrent_Logins::instance(), - 'destroy_all_sessions_but_newest' - ) - ); - $this->assertEquals( - array( - $session_2, - ), - wp_get_all_sessions() - ); - - // Now simulate the current session is the oldest. - wp_destroy_all_sessions( $user_id ); - $session_1 = $this->_log_in( $user_id, time() + ( 1 * DAY_IN_SECONDS ), $first_token ); - $session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ), $second_token ); - - // Make the session 2 the oldest: - $session_2['login'] = time() - ( 2 * DAY_IN_SECONDS ); - WP_Session_Tokens::get_instance( $user_id )->update( - $second_token, - $session_2 - ); - $this->assertEquals( - array( - $session_1, - $session_2, - ), - wp_get_all_sessions() - ); - LLMS_Prevent_Concurrent_Logins::instance()->init(); - // I expect the first session (promoted as newest) to be kept. - $this->assertEquals( - 0, - LLMS_Unit_Test_Util::call_method( - LLMS_Prevent_Concurrent_Logins::instance(), - 'destroy_all_sessions_but_newest' - ) - ); - $this->assertEquals( - array( - $session_1, - ), - wp_get_all_sessions() - ); - - } - - /** - * Simulate a log in. - * - * @since 5.6.0 - * - * @param int $user_id WP_User ID. - * @param int $epiration Expiration time. - * @param string $token Passed by reference, the created session token. - * @return array Login session. - */ - private function _log_in( $user_id, $expiration, &$token = '' ) { - - $manager = WP_Session_Tokens::get_instance( $user_id ); - $token = $manager->create( $expiration ); - wp_set_current_user( $user_id ); - $logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token ); - $this->cookies->set( LOGGED_IN_COOKIE, $logged_in_cookie, $expiration + ( 12 * HOUR_IN_SECONDS ), SITECOOKIEPATH, COOKIE_DOMAIN, false, true ); - return $manager->get( $token ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-query.php b/tests/phpunit/unit-tests/class-llms-test-query.php deleted file mode 100644 index db017da14c..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-query.php +++ /dev/null @@ -1,213 +0,0 @@ -<?php -/** - * Tests for LLMS_Query class - * - * @package LifterLMS/Tests - * - * @group query - * - * @since 4.5.0 - */ -class LLMS_Test_Query extends LLMS_UnitTestCase { - - /** - * Set up test case - * - * @since 4.5.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Query(); - - } - - /** - * Assertion helper to ensure that the `$wp_query->query` variables equal an expected array - * - * @since 5.0.2 - * - * @param array $expected Expected query array. - * @param string $message Error message. - * @return void - */ - private function assertQueryVarsEqual( $expected, $message = null ) { - - global $wp_query; - $this->assertEquals( $expected, $wp_query->query, urldecode( $message ) ); - - } - - /** - * Tests the add_endpoints() method - * - * This is a large "integration" test that ensures that student dashboard - * endpoints are properly added to the `$wp_rewrite` list. - * - * It does "real" tests by simulating a visit to the URL and testing that the expected - * `$wp_query->query` variables are set. - * - * It runs tests with the default values of the endpoints, with custom translated values in various - * languages (to ensure non-latin characters work in customized slugs) and finally adds a random string - * of alphanumeric latin chars for testing. - * - * It additionally tests pagination for endpoints that utilize pagination work regardless of the customized - * slug. - * - * @since 5.0.2 - * - * @link https://github.com/gocodebox/lifterlms/issues/1639 - * - * @return void - */ - public function test_add_endpoints() { - - LLMS_Install::create_pages(); - - // Setup. - $temp = get_option( 'permalink_structure' ); - update_option( 'permalink_structure', '/%postname%/' ); - - global $wp_rewrite; - $wp_rewrite->init(); - - $account_url = llms_get_page_url( 'myaccount' ); - $options = array( - 'view-courses' => 'lifterlms_myaccount_courses_endpoint', - 'my-grades' => 'lifterlms_myaccount_grades_endpoint', - 'view-memberships' => 'lifterlms_myaccount_memberships_endpoint', - 'view-achievements' => 'lifterlms_myaccount_achievements_endpoint', - 'view-certificates' => 'lifterlms_myaccount_certificates_endpoint', - 'notifications' => 'lifterlms_myaccount_notifications_endpoint', - 'edit-account' => 'lifterlms_myaccount_edit_account_endpoint', - 'redeem-voucher' => 'lifterlms_myaccount_redeem_vouchers_endpoint', - 'orders' => 'lifterlms_myaccount_orders_endpoint', - ); - - $non_latin = array( - 'view-courses' => 'ビューコース', // Japanese. - 'my-grades' => 'мои-оценки', // Russian. - 'view-memberships' => 'ਵੇਖੋ-ਸਦੱਸਤਾ', // Punjabi. - 'view-achievements' => 'nailiyyətlər', // Azerbaijani. - 'view-certificates' => 'ເບິ່ງໃບຢັ້ງຢືນ', // Lao. - 'notifications' => '通知', // Chinese (Simplified). - 'edit-account' => 'חשבון-עריכה', // Hebrew. - 'redeem-voucher' => 'چھڑانا', // Urdu. - 'orders' => 'आदेश', // Hindi. - ); - - foreach ( LLMS_Student_Dashboard::get_tabs() as $id => $tab ) { - - if ( empty( $tab['endpoint'] ) ) { - continue; - } - - $tests = array( - $tab['endpoint'], - wp_generate_password( 6, false, false ), - $non_latin[ $id ], - ); - - foreach ( $tests as $option ) { - - update_option( $options[ $id ], urlencode( $option ) ); - new LLMS_Student_Dashboard(); - $this->main->add_endpoints(); - flush_rewrite_rules(); - - $url = isset( $tab['url'] ) ? $tab['url'] : llms_get_endpoint_url( $id, null, $account_url ); - - $this->go_to( $url ); - - $expect = array( - 'pagename' => 'dashboard', - ); - - if ( 'dashboard' === $id ) { - $expect['page'] = ''; - } else { - $expect[ $id ] = ''; - } - - $this->assertQueryVarsEqual( $expect, $id . ' - ' . $url ); - - if ( ! empty( $tab['paginate'] ) ) { - $url .= 'page/1'; - $this->go_to( $url ); - $expect['paged'] = 1; - $this->assertQueryVarsEqual( $expect, $url ); - - } - - } - - } - - - $this->go_to( '' ); - - // Teardown. - update_option( 'permalink_structure', $temp ); - $wp_rewrite->init(); - - } - - /** - * Test maybe_404_certificate() - * - * This test runs in a separate process because something before it is making it hard - * to mock the `$wp_query` and `$post` globals. - * - * @since 4.5.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_maybe_404_certificate() { - - global $post, $wp_query; - $temp = $post; - - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - - // Not set. - $post = null; - $this->main->maybe_404_certificate(); - $this->assertFalse( $wp_query->is_404() ); - - $tests = array( - 'llms_my_certificate' => true, - 'post' => false, - 'page' => false, - 'course' => false, - ); - - foreach ( $tests as $post_type => $expect ) { - - $post = $this->factory->post->create_and_get( compact( 'post_type' ) ); - $wp_query->init(); - - // Logged out user. - $this->main->maybe_404_certificate(); - $this->assertEquals( $expect, $wp_query->is_404(), $post_type ); - - // Logged in admin can always see. - $wp_query->init(); - wp_set_current_user( $admin ); - $this->main->maybe_404_certificate(); - $this->assertFalse( $wp_query->is_404(), $post_type ); - - wp_set_current_user( null ); - - } - - $post = $temp; - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-rest.php b/tests/phpunit/unit-tests/class-llms-test-rest.php deleted file mode 100644 index 33872fe12f..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-rest.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * Test inclusion and initialization of the rest api bundle - * - * @package LifterLMS/Tests - * - * @group rest - * @group packages - * - * @since 3.36.3 - * @version 3.36.3 - */ -class LLMS_Test_REST extends LLMS_Unit_Test_Case { - - /** - * Test rest package exists and is loaded. - * - * @since 3.36.3 - * - * @return void - */ - public function test_rest_package_exists() { - $this->assertTrue( function_exists( 'LLMS_REST_API' ) ); - $this->assertTrue( defined( 'LLMS_REST_API_VERSION' ) ); - $this->assertNotNull( LLMS_REST_API_VERSION ); - } - - /** - * Ensure the REST API initializes. - * - * @since 3.36.3 - * - * @return void - */ - public function test_api_init() { - - $res = llms_rest_get_api_endpoint_data( '/llms/v1' ); - $this->assertEquals( 'llms/v1', $res['namespace'] ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-roles.php b/tests/phpunit/unit-tests/class-llms-test-roles.php deleted file mode 100644 index ff2e8469a5..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-roles.php +++ /dev/null @@ -1,206 +0,0 @@ -<?php -/** - * Tests for LifterLMS Custom Post Types - * - * @group LLMS_Roles - * - * @since 3.13.0 - * @version 4.5.1 - */ -class LLMS_Test_Roles extends LLMS_UnitTestCase { - - /** - * Tear down - * - * @since 3.28.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - parent::tear_down(); - $wp_roles = wp_roles(); - LLMS_Roles::install(); - } - - /** - * test get_all_core_caps() method - * - * @since 3.13.0 - * - * @return void - */ - public function test_get_all_core_caps() { - - $this->assertTrue( is_array( LLMS_Roles::get_all_core_caps() ) ); - $this->assertTrue( ! empty( LLMS_Roles::get_all_core_caps() ) ); - - } - - /** - * Test get_roles() method. - * - * @since 3.13.0 - * - * @return void - */ - public function test_get_roles() { - - $expect = array( - 'instructor' => __( 'Instructor', 'lifterlms' ), - 'instructors_assistant' => __( 'Instructor\'s Assistant', 'lifterlms' ), - 'lms_manager' => __( 'LMS Manager', 'lifterlms' ), - 'student' => __( 'Student', 'lifterlms' ), - ); - $this->assertEquals( $expect, LLMS_Roles::get_roles() ); - - } - - /** - * Test install_roles() method. - * - * @since 3.13.0 - * @since 3.34.0 Test for "view_students" on instructors. - * - * @return void - */ - public function test_install() { - - $wp_roles = wp_roles(); - - // Remove first. - LLMS_Roles::remove_roles(); - - // Install them. - LLMS_Roles::install(); - - // Ensure all the roles were installed. - foreach ( array_keys( LLMS_Roles::get_roles() ) as $role ) { - $this->assertTrue( $wp_roles->is_role( $role ) ); - } - - // Test admin caps were installed. - $admin = $wp_roles->get_role( 'administrator' ); - - foreach ( LLMS_Roles::get_all_core_caps() as $cap ) { - $this->assertTrue( $admin->has_cap( $cap ) ); - } - - // Test instructor caps. - $instructor = $wp_roles->get_role( 'instructor' ); - foreach ( LLMS_Roles::get_all_core_caps() as $cap ) { - $has = $instructor->has_cap( $cap ); - if ( in_array( $cap, array( 'view_lifterlms_reports', 'lifterlms_instructor', 'view_students' ) ) ) { - $this->assertTrue( $has ); - } else { - $this->assertFalse( $has ); - } - } - - } - - /** - * Test remove_roles() method. - * - * @since 3.13.0 - * @since 3.28.0 Unknown. - * @since 4.5.1 Make sure only custom roles are removed from the 'adminitrator' role. - * - * @return void - */ - public function test_remove_roles() { - - $wp_roles = wp_roles(); - - // Remove them. - LLMS_Roles::remove_roles(); - - // Make sure roles are gone. - foreach ( array_keys( LLMS_Roles::get_roles() ) as $role ) { - $this->assertFalse( $wp_roles->is_role( $role ) ); - } - - // Test admin custom caps were removed. - $admin = $wp_roles->get_role( 'administrator' ); - $admin_caps = LLMS_Unit_Test_Util::call_method( 'LLMS_Roles', 'get_all_caps', array( 'administrator') ); - $wp_caps = $admin_caps['wp']; - - foreach ( $admin_caps as $group => $caps ) { - foreach ( array_keys( $caps ) as $cap ) { - if ( 'wp' === $group ) { - $this->assertTrue( $admin->has_cap( $cap ) ); - } else { - $this->assertFalse( $admin->has_cap( $cap ) ); - } - } - } - - } - - /** - * Test get_all_role_names() method. - * - * @since 5.6.0 - * - * @return void - */ - public function test_get_all_role_names() { - - $wp_roles = array( - 'administrator' => 'Administrator', - 'editor' => 'Editor', - 'author' => 'Author', - 'contributor' => 'Contributor', - 'subscriber' => 'Subscriber', - ); - $llms_roles = array( - 'lms_manager' => 'LMS Manager', - 'instructor' => 'Instructor', - 'instructors_assistant' => 'Instructor\'s Assistant', - 'student' => 'Student', - ); - - $expect = array_merge( $wp_roles, $llms_roles ); - - $translated_roles = array_combine( - array_keys( $expect ), - array_map( - function( $role_name ) { - return "Translated {$role_name}"; - }, - $expect - ) - ); - - $translations = array_combine( - array_values( $expect ), - array_values( $translated_roles ) - ); - - $this->assertEquals( $expect, LLMS_Roles::get_all_role_names() ); - - // Simulate a different language. - // For wp roles. - $gettext_with_context = function( $translation, $text, $context, $domain ) use ( $wp_roles, $translations ) { - if ( 'User role' === $context && 'default' === $domain && in_array( $text, $wp_roles, true ) ) { - return $translations[ $text ]; - } - return $translation; - }; - // For our roles. - $gettext = function( $translation, $text, $domain ) use ( $llms_roles, $translations ) { - if ( 'lifterlms' === $domain && in_array( $text, $llms_roles, true ) ) { - return $translations[ $text ]; - } - return $translation; - }; - - add_filter( 'gettext_with_context', $gettext_with_context, 10, 4 ); - add_filter( 'gettext', $gettext, 10, 3 ); - $this->assertEquals( $translated_roles , LLMS_Roles::get_all_role_names() ); - remove_filter( 'gettext_with_context', $gettext_with_context, 10, 4 ); - remove_filter( 'gettext', $gettext, 10, 3 ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-session.php b/tests/phpunit/unit-tests/class-llms-test-session.php deleted file mode 100644 index 03ed4887e8..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-session.php +++ /dev/null @@ -1,323 +0,0 @@ -<?php -/** - * Test session class - * - * @package LifterLMS/Tests - * - * @group session - * @group sessions - * - * @since 4.0.0 - */ -class LLMS_Test_Session extends LLMS_Unit_Test_Case { - - /** - * Setup test - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Session(); - - } - - /** - * Retrieve the name of the cookie - * - * @since 4.0.0 - * - * @return string - */ - protected function get_cookie_name() { - return LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cookie' ); - } - - /** - * Retrieve the raw cookie value. - * - * @since 4.0.0 - * - * @return array - */ - protected function get_raw_cookie() { - return $this->cookies->get( $this->get_cookie_name() ); - } - - /** - * Test constructor - * - * @since 4.0.0 - * - * @return void - */ - public function test_construct_should_init() { - - remove_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ); - remove_action( 'wp_logout', array( $this->main, 'destroy' ) ); - remove_action( 'shutdown', array( $this->main, 'maybe_save_data' ), 20 ); - - $this->main = new LLMS_Session(); - - $this->assertEquals( 10, has_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ) ); - $this->assertEquals( 10, has_action( 'wp_logout', array( $this->main, 'destroy' ) ) ); - $this->assertEquals( 20, has_action( 'shutdown', array( $this->main, 'maybe_save_data' ) ) ); - - $this->assertEquals( sprintf( 'wp_llms_session_%s', COOKIEHASH ), $this->get_cookie_name() ); - - } - - /** - * Test constructor when we should not initialize. - * - * @since 4.0.0 - * - * @return void - */ - public function test_construct_should_not_init() { - - remove_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ); - remove_action( 'wp_logout', array( $this->main, 'destroy' ) ); - remove_action( 'shutdown', array( $this->main, 'maybe_save_data' ), 20 ); - - add_filter( 'llms_session_should_init', '__return_false' ); - $this->main = new LLMS_Session(); - remove_filter( 'llms_session_should_init', '__return_false' ); - - $this->assertEquals( 10, has_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ) ); - $this->assertFalse( has_action( 'wp_logout', array( $this->main, 'destroy' ) ) ); - $this->assertFalse( has_action( 'shutdown', array( $this->main, 'maybe_save_data' ) ) ); - - $this->assertEquals( sprintf( 'wp_llms_session_%s', COOKIEHASH ), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cookie' ) ); - - } - - /** - * Test destroy() - * - * @since 4.0.0 - * - * @return void - */ - public function test_destroy() { - - $this->main->set( 'somedata', 'isset' ); - $this->assertTrue( $this->main->save( time() + HOUR_IN_SECONDS ) ); - - // Destroyed. - $this->assertTrue( $this->main->destroy() ); - - // Class properties reset. - $this->assertEquals( '', LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'id' ) ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'data' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) ); - - // Cookie should be emptied and set to expire. - $cookie = $this->get_raw_cookie(); - - $this->assertEquals( '', $cookie['value'] ); - $this->assertTrue( $cookie['expires'] < time() ); - - } - - /** - * Test get_cookie() when there's no cookie set. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_cookie_not_set() { - - $this->cookies->unset_all(); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - - } - - /** - * Test get_cookie() when it returns something unexpected - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_cookie_not_string() { - - $this->cookies->set( $this->get_cookie_name(), 1234 ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - - } - - /** - * Test get_cookie() when it's missing required parts. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_cookie_missing_parts() { - - $this->cookies->set( $this->get_cookie_name(), 'part1' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - - $this->cookies->set( $this->get_cookie_name(), 'part1||part2||part3||' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - - } - - /** - * Test get_cookie() when the hash is invalid - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_cookie_invalid() { - - $this->cookies->set( $this->get_cookie_name(), 'part1||part2||part3||part4|1234' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - - } - - /** - * Test get_cookie() for a success return - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_cookie() { - - $parts = LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ); - - $this->assertEquals( $this->main->get_id(), $parts[0] ); - $this->assertEquals( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expires' ), $parts[1] ); - $this->assertEquals( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ), $parts[2] ); - $this->assertTrue( is_string( $parts[3] ) ); - - } - - /** - * Test init_cookie() when the cookie exists - * - * @since 4.0.0 - * - * @return void - */ - public function test_init_cookie_from_existing() { - - $data = $this->main->set( 'something', 123 ); - $this->main->save( time() + HOUR_IN_SECONDS ); - $parts = LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ); - - // Reset everything. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'id', '' ); - LLMS_Unit_Test_Util::set_private_property( $this->main, 'expires', 0 ); - LLMS_Unit_Test_Util::set_private_property( $this->main, 'expiring', 0 ); - LLMS_Unit_Test_Util::set_private_property( $this->main, 'data', array() ); - - // Reinit. - LLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' ); - - $this->assertEquals( $parts, LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) ); - $this->assertEquals( $parts[0], $this->main->get_id() ); - $this->assertEquals( $parts[1], LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expires' ) ); - $this->assertEquals( $parts[2], LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ) ); - $this->assertEquals( 123, $this->main->get( 'something' ) ); - - } - - /** - * Test init_cookie() when the cookie is expiring - * - * @since 4.0.0 - * - * @return void - */ - public function test_init_cookie_from_existing_expiring() { - - // Expiring is in the past. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'expiring', 0 ); - - // Reinit. - LLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' ); - - // Expiring reset to the future. - $this->assertTrue( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ) > time() ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' )[2] > time() ); - - } - - /** - * Test init_cookie() when the user id is to change - * - * @since 4.0.0 - * - * @return void - */ - public function test_init_cookie_from_existing_user_logged_in() { - - $id = $this->main->get_id(); - $uid = $this->factory->user->create(); - - wp_set_current_user( $uid ); - - // Reinit. - LLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' ); - - $this->assertEquals( $uid, $this->main->get_id() ); - $this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' )[0] ); - - } - - /** - * Test init_cookie() when a new cookie is created - * - * @since 4.0.0 - * - * @return void - */ - public function test_init_cookie_new() { - - $original = $this->get_raw_cookie(); - $this->cookies->unset_all(); - - LLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' ); - $this->assertNotEquals( $original, $this->get_raw_cookie() ); - - - } - - /** - * Test - * - * @since 4.0.0 - * - * @return void - */ - public function test_maybe_save_data_is_clean() { - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true ); - $this->assertFalse( $this->main->maybe_save_data() ); - - } - - /** - * Test - * - * @since 4.0.0 - * - * @return void - */ - public function test_maybe_save_data_is_not_clean() { - - $this->main->set( 'test', 'data' ); - $this->assertTrue( $this->main->maybe_save_data() ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-sessions.php b/tests/phpunit/unit-tests/class-llms-test-sessions.php deleted file mode 100644 index 34f4926cd4..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-sessions.php +++ /dev/null @@ -1,620 +0,0 @@ -<?php -/** - * Test sessions class - * - * @package LifterLMS/Tests - * - * @group sessions - * - * @since 3.36.0 - * @version 4.5.0 - */ -class LLMS_Test_Sessions extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.36.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->sessions = LLMS_Sessions::instance(); - - - } - - /** - * Test get_open_sessions() - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_open_sessions() { - - $time = time(); - - $i = 0; - while ( $i < 5 ) { - - wp_set_current_user( $this->factory->user->create() ); - - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - $this->sessions->start(); - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - $this->sessions->end_current(); - - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - - $this->sessions->start(); - - $i++; - - } - - $sessions = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_open_sessions' ); - $this->assertEquals( 5, count( $sessions ) ); - - foreach ( $sessions as $session ) { - $this->assertEquals( 2, $session->get( 'object_id' ) ); - $this->assertTrue( $this->sessions->is_session_open( $session ) ); - } - - } - - /** - * Setup end_idle_sessions() - * - * @since 3.36.0 - * - * @return void - */ - public function test_end_idle_sessions() { - - $time = time(); - - $started = array(); - - $i = 0; - while ( $i < 5 ) { - - wp_set_current_user( $this->factory->user->create() ); - - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - $started[] = $this->sessions->start(); - - $i++; - - } - - // It hasn't been long enough. - $this->sessions->end_idle_sessions(); - foreach ( $started as $i => $session ) { - $this->assertTrue( $this->sessions->is_session_open( $session ) ); - } - - $this->assertEquals( 4, $i ); - - $time += HOUR_IN_SECONDS; - llms_tests_mock_current_time( $time ); - $this->sessions->end_idle_sessions(); - foreach ( $started as $i => $session ) { - $this->assertFalse( $this->sessions->is_session_open( $session ) ); - } - - $this->assertEquals( 4, $i ); - - } - - /** - * Test end_current() - * - * @since 3.36.0 - * - * @return void - */ - public function test_end_current() { - - wp_set_current_user( $this->factory->user->create() ); - $start = $this->sessions->start(); - - $end = $this->sessions->end_current(); - - $this->assertTrue( is_a( $end, 'LLMS_Event' ) ); - $this->assertEquals( $start->get( 'actor_id' ), $end->get( 'actor_id' ) ); - $this->assertEquals( 'session', $end->get( 'event_type' ) ); - $this->assertEquals( 'end', $end->get( 'event_action' ) ); - $this->assertEquals( 'session', $end->get( 'object_type' ) ); - $this->assertEquals( $start->get( 'object_id' ), $end->get( 'object_id' ) ); - - } - - /** - * Test get_new_session_id() - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_new_session_id() { - - wp_set_current_user( $this->factory->user->create() ); - - $this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id' ) ); - $this->sessions->start(); - - $this->assertEquals( 2, LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id' ) ); - - } - - /** - * Test get_current() when there's no logged in user - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_current_no_user() { - - $this->assertFalse( $this->sessions->get_current() ); - - } - - /** - * Test get_current() when user has no previous sessions - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_current_no_previous_sessions() { - - wp_set_current_user( $this->factory->user->create() ); - $this->assertFalse( $this->sessions->get_current() ); - - } - - /** - * Test get_current() when there's an open session - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_current_is_open() { - - wp_set_current_user( $this->factory->user->create() ); - $event = $this->sessions->start(); - - $current = $this->sessions->get_current(); - $this->assertEquals( $event->get( 'id' ), $current->get( 'id' ) ); - - } - - /** - * Test get_current() when the most recent session is closed - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_current_last_is_closed() { - - wp_set_current_user( $this->factory->user->create() ); - $this->sessions->start(); - $this->sessions->end_current(); - - $this->assertFalse( $this->sessions->get_current() ); - - } - - /** - * Test get_session_end() when there's no end event for the session - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_session_end_no_end() { - - wp_set_current_user( $this->factory->user->create() ); - $start = $this->sessions->start(); - - $this->assertNull( $this->sessions->get_session_end( $start ) ); - - } - - /** - * Test get_session_end() - * - * @since 3.36.0 - * - * @return void - */ - public function test_get_session_end() { - - wp_set_current_user( $this->factory->user->create() ); - $start = $this->sessions->start(); - $end = $this->sessions->end_current(); - - $test_end = $this->sessions->get_session_end( $start ); - - $this->assertTrue( is_a( $test_end, 'LLMS_Event' ) ); - $this->assertEquals( $end->get( 'id' ), $test_end->get( 'id' ) ); - - } - - /** - * Test get_session_events() - * - * @since 3.36.0 - * @since 3.37.15 Updated to take into account the page.* events removal. - * - * @return void - */ - public function test_get_session_events() { - - add_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) ); - LLMS()->events()->register_events(); - - $start_time = time() - HOUR_IN_SECONDS; - llms_tests_mock_current_time( $start_time ); - - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - // Start session. - $start = $this->sessions->start(); - - llms_tests_mock_current_time( $start_time + MINUTE_IN_SECONDS ); - - // Create events. - LLMS()->events()->record( array( - 'actor_id' => $user, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'load', - ) ); - - llms_tests_mock_current_time( $start_time + ( MINUTE_IN_SECONDS * 2 ) ); - - LLMS()->events()->record( array( - 'actor_id' => $user, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'exit', - ) ); - - // Return those events during an open session. - $sessions = $this->sessions->get_session_events( $start ); - $this->assertEquals( 2, count( $sessions ) ); - - foreach ( $sessions as $event ) { - $this->assertTrue( is_a( $event, 'LLMS_Event' ) ); - $this->assertEquals( $user, $event->get( 'actor_id' ) ); - $this->assertEquals( 1, $event->get( 'object_id' ) ); - $this->assertEquals( 'page', $event->get( 'event_type' ) ); - $this->assertEquals( 'post', $event->get( 'object_type' ) ); - } - - llms_tests_mock_current_time( $start_time + ( MINUTE_IN_SECONDS * 3 ) ); - - // End the session. - $this->sessions->end_current(); - - // Add a new event (new session) - LLMS()->events()->record( array( - 'actor_id' => $user, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'exit', - ) ); - - // Original session should still only return 2 events. - $sessions = $this->sessions->get_session_events( $start ); - $this->assertEquals( 2, count( $sessions ) ); - - remove_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) ); - - } - - /** - * Test is_session_idle() on an already closed session - * - * @since 3.36.0 - * - * @return void - */ - public function test_is_session_idle_already_closed() { - - wp_set_current_user( $this->factory->user->create() ); - - // Start session. - $start = $this->sessions->start(); - $this->sessions->end_current(); - - // This session has already ended. - $this->assertFalse( $this->sessions->is_session_idle( $start ) ); - - } - - /** - * Test is_session_idle() on a session that started less than 30 minutes ago - * - * @since 3.36.0 - * - * @return void - */ - public function test_is_session_idle_started_within_window() { - - wp_set_current_user( $this->factory->user->create() ); - - // Start session. - $start = $this->sessions->start(); - - $this->assertFalse( $this->sessions->is_session_idle( $start ) ); - - llms_tests_mock_current_time( time() + ( 29 * MINUTE_IN_SECONDS ) ); - $this->assertFalse( $this->sessions->is_session_idle( $start ) ); - - } - - /** - * Test is_session_idle() for a session that started more than 30 minutes ago and has no events - * - * @since 3.36.0 - * - * @return void - */ - public function test_is_session_idle_old_with_no_events() { - - wp_set_current_user( $this->factory->user->create() ); - - // Start session. - $start = $this->sessions->start(); - - // Session is older than 30 minutes or older & has no events. - llms_tests_mock_current_time( time() + ( 31 * MINUTE_IN_SECONDS ) ); - $this->assertTrue( $this->sessions->is_session_idle( $start ) ); - - } - - - /** - * Test is_session_idle() for a session that started more than 30 minutes ago - * and has at least one active event that's less than 30 minutes old - * - * @since 3.36.0 - * @since 3.37.15 Updated to take into account the page.* events removal. - * - * @return void - */ - public function test_is_session_idle_old_with_events_within_window() { - - add_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) ); - LLMS()->events()->register_events(); - - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - // Start session. - $start = $this->sessions->start(); - - llms_tests_mock_current_time( time() + ( 10 * MINUTE_IN_SECONDS ) ); - - // Add a new - LLMS()->events()->record( array( - 'actor_id' => $user, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'exit', - ) ); - - llms_tests_mock_current_time( time() + ( 31 * MINUTE_IN_SECONDS ) ); - $this->assertFalse( $this->sessions->is_session_idle( $start ) ); - - remove_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) ); - } - - - /** - * 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. - * - * @since 3.36.0 - * - * @return void - */ - public function test_is_session_idle_old_with_events_outside_window() { - - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - // Start session. - $start = $this->sessions->start(); - - llms_tests_mock_current_time( time() + ( 15 * MINUTE_IN_SECONDS ) ); - - // Add a new - LLMS()->events()->record( array( - 'actor_id' => $user, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'exit', - ) ); - - // Session is older than 30 minutes and last event within the session is older than 30 mins. - llms_tests_mock_current_time( time() + ( 46 * MINUTE_IN_SECONDS ) ); - $this->assertTrue( $this->sessions->is_session_idle( $start ) ); - - } - - /** - * Test start() when no user - * - * @since 3.36.0 - * - * @return void - */ - public function test_start_no_user() { - - $this->assertFalse( $this->sessions->start() ); - - } - - /** - * Test is_session_open() - * - * @since 3.36.0 - * - * @return void - */ - public function test_is_session_open() { - - wp_set_current_user( $this->factory->user->create() ); - $start = $this->sessions->start(); - - $this->assertTrue( $this->sessions->is_session_open( $start ) ); - $this->sessions->end_current(); - - $this->assertFalse( $this->sessions->is_session_open( $start ) ); - - } - - /** - * Test start() - * - * @since 3.36.0 - * - * @return void - */ - public function test_start() { - - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - $event = $this->sessions->start(); - - $this->assertTrue( is_a( $event, 'LLMS_Event' ) ); - $this->assertEquals( $user, $event->get( 'actor_id' ) ); - $this->assertEquals( 'session', $event->get( 'event_type' ) ); - $this->assertEquals( 'start', $event->get( 'event_action' ) ); - $this->assertEquals( 'session', $event->get( 'object_type' ) ); - $this->assertTrue( is_numeric( $event->get( 'object_id' ) ) ); - - } - - /** - * Test session starts on user login - * - * @since 4.5.0 - * - * @return void - */ - public function test_on_wp_login_action() { - - $user = $this->factory->user->create_and_get( - array( - 'user_pass' => 'user_pass', - ) - ); - $wp_login_count = did_action( 'wp_login' ); - - // Test there's no current session. - $this->assertFalse( $this->sessions->get_current() ); - - // Simulate wp login that will trigger the `wp_login` action without setting the current user though. - wp_signon( - array( - 'user_login' => $user->user_login, - 'user_password' => 'user_pass', - ) - ); - $this->assertEquals( $wp_login_count + 1, did_action( 'wp_login' ) ); - - // Set the current user. - wp_set_current_user( $user->ID ); - - $start_session = $this->sessions->get_current(); - - // A new session has been created. - $this->assertTrue( is_a( $start_session, 'LLMS_Event' ) ); - - // And it's the correct one. - $this->assertEquals( $user->ID, $start_session->get( 'actor_id' ) ); - $this->assertEquals( 'session', $start_session->get( 'object_type' ) ); - $this->assertEquals( 'session', $start_session->get( 'event_type' ) ); - $this->assertEquals( 'start', $start_session->get( 'event_action' ) ); - - // Clean the opened session. - $this->sessions->end_current(); - } - - /** - * Test session ends on user logout - * - * @since 4.5.0 - * - * @return void - */ - public function test_on_signout() { - - $user = $this->factory->user->create_and_get( - array( - 'user_pass' => 'user_pass', - ) - ); - - wp_set_current_user( $user->ID ); - $start_session = $this->sessions->start(); - // A new session has been created and it's the current one. - $this->assertTrue( is_a( $start_session, 'LLMS_Event' ) ); - $this->assertTrue( $this->sessions->is_session_open( $start_session ) ); - - $current_session = $this->sessions->get_current(); - $this->assertEquals( $current_session->get( 'id' ), $start_session->get( 'id' ) ); - - // Simulate sign out. - do_action( 'clear_auth_cookie' ); - // No current session. - $current_session = $this->sessions->get_current(); - $this->assertFalse( $current_session ); - // Previously started session correctly ended. - $this->assertFalse( $this->sessions->is_session_open( $start_session ) ); - - } - - /** - * Allow page events for testing purposes. - * - * @since 3.37.15 - * - * @param array $allowed_events Array of allowed events - * @return array - */ - public function allow_page_events_for_testing( $allowed_events ) { - - return array_merge( - $allowed_events, - array( - 'page.load' => true, - 'page.exit' => true, - 'page.focus' => true, - 'page.blur' => true, - ) - ); - - } -} diff --git a/tests/phpunit/unit-tests/class-llms-test-shortcodes.php b/tests/phpunit/unit-tests/class-llms-test-shortcodes.php deleted file mode 100644 index 2113bfd94a..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-shortcodes.php +++ /dev/null @@ -1,140 +0,0 @@ -<?php -/** - * Test LifterLMS Shortcodes - * - * @package LifterLMS/Tests - * - * @group shortcodes - * - * @since 3.4.3 - * @since 3.24.1 Unknown. - * @since 4.0.0 Add tests for `get_course_id()` method. - * @since 5.0.0 Don't need to test for password strength enqueue anymore. - */ -class LLMS_Test_Shortcodes extends LLMS_UnitTestCase { - - /** - * Test the private get_course_id() method used by various legacy shortcodes. - * - * @since 4.0.0 - * - * @return void - */ - public function test_get_course_id() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'questions' => 1 ) ); - - $course_id = $course->get( 'id' ); - - // On course. - $this->go_to( get_permalink( $course_id ) ); - $this->assertTrue( is_course() ); - $this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) ); - - // On lesson. - $lesson = $course->get_lessons()[0]; - $this->go_to( get_permalink( $lesson->get( 'id' ) ) ); - $this->assertTrue( is_lesson() ); - $this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) ); - - // On quiz. - $this->go_to( get_permalink( $lesson->get( 'quiz' ) ) ); - $this->assertTrue( is_quiz() ); - $this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) ); - - } - - /** - * Generic tests and a few tests on the abstract - * @return void - * @since 3.4.3 - * @version 3.24.1 - */ - public function test_shortcodes() { - - $shortcodes = array( - 'LLMS_Shortcode_Course_Author', - 'LLMS_Shortcode_Course_Continue', - 'LLMS_Shortcode_Course_Meta_Info', - 'LLMS_Shortcode_Course_Outline', - 'LLMS_Shortcode_Course_Prerequisites', - 'LLMS_Shortcode_Course_Reviews', - 'LLMS_Shortcode_Course_Syllabus', - 'LLMS_Shortcode_Membership_Link', - 'LLMS_Shortcode_Registration', - ); - - foreach ( $shortcodes as $class ) { - - $obj = $class::instance(); - $this->assertTrue( shortcode_exists( $obj->tag ) ); - $this->assertTrue( is_a( $obj, 'LLMS_Shortcode' ) ); - $this->assertTrue( ! empty( $obj->tag ) ); - $this->assertTrue( is_string( $obj->output() ) ); - $this->assertTrue( is_array( $obj->get_attributes() ) ); - $this->assertTrue( is_string( $obj->get_content() ) ); - - } - - $this->assertClassHasStaticAttribute( '_instances', 'LLMS_Shortcode' ); - - } - - /** - * Test the registration shortcode - * - * @since 3.4.3 - * @since 4.4.0 Use `LLMS_Assets::is_inline_enqueued()` in favor of deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()`. - * @since 5.0.0 Don't need to test for password strength enqueue anymore. - * @since 5.3.3 Use `assertStringContains()` in favor of `assertContains()`. - * - * @return void - */ - public function test_registration() { - - // our output should enqueue this - wp_dequeue_script( 'password-strength-meter' ); - - $obj = LLMS_Shortcode_Registration::instance(); - - // when logged out, there should be html content - $this->assertStringContains( 'llms-new-person-form-wrapper', $obj->output() ); - - // no html when logged in - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $user_id ); - $this->assertEmpty( $obj->output() ); - - } - - /** - * Test lifterlms_membership_link shortcode - * - * @since 3.4.3 - * @since 5.3.3 Use `assertStringContains()` in favor of `assertContains()`. - * - * @return void - */ - public function test_membership_link() { - - // create a membership that we can use for linking - $mid = $this->factory->post->create( array( - 'post_title' => 'Test Membership', - 'post_type' => 'llms_membership', - ) ); - - $obj = LLMS_Shortcode_Membership_Link::instance(); - - // test default settings - $this->assertStringContains( get_permalink( $mid ), $obj->output( array( 'id' => $mid ) ) ); - $this->assertStringContains( get_the_title( $mid ), $obj->output( array( 'id' => $mid ) ) ); - - $this->assertEquals( $mid, $obj->get_attribute( 'id' ) ); - - // check non default content - $this->assertStringContains( 'Alternate Text', $obj->output( array( 'id' => $mid ), 'Alternate Text' ) ); - $this->assertEquals( 'Alternate Text', $obj->get_content( 'Alternate Text' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-site.php b/tests/phpunit/unit-tests/class-llms-test-site.php deleted file mode 100644 index 2788231638..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-site.php +++ /dev/null @@ -1,239 +0,0 @@ -<?php -/** - * Tests for LLMS_Site - * - * @package LifterLMS/Tests - * - * @group site - * - * @since 3.7.4 - */ -class LLMS_Test_Site extends LLMS_UnitTestCase { - - /** - * Test clear_lock_url() function - * - * @since 3.8.0 - * - * @return void - */ - public function test_clear_lock_url() { - - update_option( 'llms_site_url', 'http://mockurl.tld/' ); - LLMS_Site::clear_lock_url(); - $this->assertEquals( '', get_option( 'llms_site_url' ) ); - - } - - /** - * Test check_status() method - * - * @since 4.12.0 - * - * @return void - */ - public function test_check_status() { - - $actions = did_action( 'llms_site_clone_detected' ); - $original = get_site_url(); - - // Not a clone. - $this->assertFalse( LLMS_Site::check_status() ); - - // Simulate the site being cloned. - update_option( 'siteurl', 'http://fakeurl.tld' ); - - $this->assertTrue( LLMS_Site::check_status() ); - $this->assertSame( ++$actions, did_action( 'llms_site_clone_detected' ) ); - - // Site has been ignored. - update_option( 'llms_site_url_ignore', 'yes' ); - $this->assertFalse( LLMS_Site::check_status() ); - - // Restore URL. - update_option( 'siteurl', $original ); - - } - - /** - * Test lock url getter and setter functions - * - * @since 3.8.0 - * @since 4.12.0 Added urls with "www". - * @since 5.9.0 Pass an explicit integer to `substr_replace()`. - * - * @return void - */ - public function test_get_set_lock_url() { - - $urls = array( - 'https://whatever.com', - 'http://whatever.com', - 'https://www.whatever.com', - 'http://www.whatever.com', - 'https://w.com', - 'https://whatever-with-a-dash.net', - 'http://wh.at', - 'http://wah.tld', - 'http://waht.tld', - ); - - foreach ( $urls as $url ) { - - update_option( 'siteurl', $url ); - - $site_url = get_site_url(); - - // This is what the lock url should be. - $lock_url = substr_replace( $site_url, LLMS_Site::$lock_string, intval( strlen( $site_url ) / 2 ), 0 ); - - // Make sure they match. - $this->assertEquals( $lock_url, LLMS_Site::get_lock_url() ); - - // Save it. - LLMS_Site::set_lock_url(); - - // Make sure it saves the right option. - $this->assertEquals( $lock_url, get_option( 'llms_site_url' ) ); - - // This should match the original URL. - $this->assertEquals( $site_url, LLMS_Site::get_url() ); - - } - - } - - /** - * Test feature getter and setter functions - * - * @since 3.8.0 - * @since 4.12.0 Test against feature constants. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_get_set_features() { - - // Should return an array of defaults even when option doesnt exist. - delete_option( 'llms_site_get_features' ); - $this->assertTrue( is_array( LLMS_Site::get_features() ) ); - - // Fake feature always returns false. - $this->assertFalse( LLMS_Site::get_feature( 'mock_feature' ) ); - - // Test getters/setters with a real feature. - LLMS_Site::update_feature( 'recurring_payments', true ); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - - LLMS_Site::update_feature( 'recurring_payments', false ); - $this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) ); - - // Constant not set. - $this->assertNull( LLMS_Unit_Test_Util::call_method( 'LLMS_Site', 'get_feature_constant', array( 'recurring_payments' ) ) ); - $this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) ); - - // Constant is set. - llms_maybe_define_constant( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', true ); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - - } - - - /** - * Test is_clone() function - * - * @since 3.7.4 - * - * @return void - */ - public function test_is_clone() { - - $original = get_site_url(); - - // Not a clone because the url is the lock url. - $this->assertFalse( LLMS_Site::is_clone() ); - - // The url has changed. - update_option( 'siteurl', 'http://fakeurl.tld' ); - $this->assertTrue( LLMS_Site::is_clone() ); - - // Change it back to the original. - update_option( 'siteurl', $original ); - $this->assertFalse( LLMS_Site::is_clone() ); - - // Change the schema (should not be identified as a clone). - update_option( 'siteurl', set_url_scheme( $original, 'https' ) ); - $this->assertFalse( LLMS_Site::is_clone() ); - - } - - /** - * Test is_clone() when using a constant set to `true`. - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_is_clone_constant_true() { - - // Not a clone. - $this->assertFalse( LLMS_Site::is_clone() ); - - define( 'LLMS_SITE_IS_CLONE', true ); - $this->assertTrue( LLMS_Site::is_clone() ); - - } - - /** - * Test is_clone() when using a constant set to `false`. - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_is_clone_constant_false() { - - $original = get_site_url(); - - // Is a clone. - update_option( 'siteurl', 'http://fakeurl.tld' ); - $this->assertTrue( LLMS_Site::is_clone() ); - - define( 'LLMS_SITE_IS_CLONE', false ); - $this->assertFalse( LLMS_Site::is_clone() ); - - update_option( 'siteurl', $original ); - - } - - /** - * Test is_clone_ignored() function - * - * @since 3.8.0 - * - * @return void - */ - public function test_is_clone_ignored() { - - $this->assertFalse( LLMS_Site::is_clone_ignored() ); - - update_option( 'llms_site_url_ignore', 'yes' ); - $this->assertTrue( LLMS_Site::is_clone_ignored() ); - - update_option( 'llms_site_url_ignore', 'no' ); - $this->assertFalse( LLMS_Site::is_clone_ignored() ); - - update_option( 'llms_site_url_ignore', 'mock' ); - $this->assertFalse( LLMS_Site::is_clone_ignored() ) ; - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-staging.php b/tests/phpunit/unit-tests/class-llms-test-staging.php deleted file mode 100644 index 438e28e4b0..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-staging.php +++ /dev/null @@ -1,349 +0,0 @@ -<?php -/** - * Test LLMS_Staging class - * - * @package LifterLMS/Tests - * - * @group staging - * - * @since 4.12.0 - */ -class LLMS_Test_Staging extends LLMS_Unit_Test_Case { - - /** - * Setup before class - * - * @since 4.12.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php'; - - } - - /** - * Removes actions added by the `init()` method (so that we can test the `init()` method) - * - * @since 4.13.0 - * - * @return void - */ - private function remove_init_actions() { - remove_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ), 10 ); - remove_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ), 10 ); - remove_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ), 10 ); - } - - /** - * Test init() actions when no recurring feature constant is set - * - * @since 4.13.0 - * - * @return void - */ - public function test_init() { - - $this->remove_init_actions(); - - LLMS_Staging::init(); - - $this->assertEquals( 10, has_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ) ) ); - $this->assertEquals( 10, has_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ) ) ); - - $this->assertEquals( 10, has_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ) ) ); - - } - - /** - * Test init() actions when a recurring feature constant is set - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_init_with_constant() { - - $this->remove_init_actions(); - - define( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', true ); - - LLMS_Staging::init(); - - $this->assertFalse( has_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ) ) ); - $this->assertFalse( has_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ) ) ); - - $this->assertEquals( 10, has_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ) ) ); - - } - - /** - * Test init() actions when a recurring feature constant is set - * - * @since 4.13.0 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_init_clone_site_feature_cascade() { - - define( 'LLMS_SITE_IS_CLONE', true ); - - LLMS_Staging::init(); - - $this->assertFalse( LLMS_SITE_FEATURE_RECURRING_PAYMENTS ); - - } - - /** - * Test clone_detected() - * - * @since 4.12.0 - * @since 4.13.0 Add tests for all potential conditions. - * - * @return void - */ - public function test_clone_detected() { - - LLMS_Site::update_feature( 'recurring_payments', true ); - - // Not admin panel. - LLMS_Staging::clone_detected(); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - - // Admin panel but not admin. - set_current_screen( 'admin.php' ); - LLMS_Staging::clone_detected(); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - - // Admin panel and admin but doing an ajax request. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - add_filter( 'wp_doing_ajax', '__return_true' ); - LLMS_Staging::clone_detected(); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - - // All good. - remove_filter( 'wp_doing_ajax', '__return_true' ); - LLMS_Staging::clone_detected(); - $this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - LLMS_Admin_Notices::delete_notice( 'maybe-staging' ); - - // Return to front. - set_current_screen( 'front' ); - - } - - /** - * Test handle_staging_notice_actions() when the method isn't called - * - * @since 4.12.0 - * - * @return void - */ - public function test_handle_staging_notice_actions_not_called() { - - $this->assertNull( LLMS_Staging::handle_staging_notice_actions() ); - - } - - /** - * Test handle_staging_notice_actions() with an invalid nonce. - * - * @since 4.12.0 - * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation. - * - * @return void - */ - public function test_handle_staging_notice_actions_invalid_nonce() { - - $this->expectException( 'WPDieException' ); - - $this->mockGetRequest( array( - 'llms-staging-status' => 'enable', - '_llms_staging_nonce' => 'fake', - ) ); - - LLMS_Staging::handle_staging_notice_actions(); - - } - - /** - * Test handle_staging_notice_actions() with an invalid user. - * - * @since 4.12.0 - * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation. - * - * @return void - */ - public function test_handle_staging_notice_actions_invalid_user() { - - $this->expectException( 'WPDieException' ); - - $this->mockGetRequest( array( - 'llms-staging-status' => 'enable', - '_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ), - ) ); - - LLMS_Staging::handle_staging_notice_actions(); - - } - - /** - * Test handle_staging_notice_actions() when enabling recurring payments - * - * @since 4.12.0 - * - * @return void - */ - public function test_handle_staging_notice_actions_enable() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $_SERVER['HTTP_REFERER'] = 'http://example.tld/wp-admin/?page=whatever'; - $original = get_site_url(); - update_option( 'siteurl', 'http://fakeurl.tld' ); - LLMS_Site::update_feature( 'recurring_payments', false ); - - $this->mockGetRequest( array( - 'llms-staging-status' => 'enable', - '_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ), - ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( $_SERVER['HTTP_REFERER'] . ' [302] YES' ); - - try { - - LLMS_Staging::handle_staging_notice_actions(); - - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $this->assertEquals( get_option( 'llms_site_url' ), LLMS_Site::get_lock_url() ); - $this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - - update_option( 'siteurl', $original ); - unset( $_SERVER['HTTP_REFERER'] ); - - throw $exception; - } - - } - - /** - * Test handle_staging_notice_actions() when enabling recurring payments - * - * @since 4.12.0 - * - * @return void - */ - public function test_handle_staging_notice_actions_disable() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $_SERVER['HTTP_REFERER'] = 'http://example.tld/wp-admin/?page=whatever'; - $original = get_site_url(); - update_option( 'siteurl', 'http://fakeurl.tld' ); - LLMS_Site::update_feature( 'recurring_payments', true ); - - $this->mockGetRequest( array( - 'llms-staging-status' => 'disable', - '_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ), - ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( $_SERVER['HTTP_REFERER'] . ' [302] YES' ); - - try { - - LLMS_Staging::handle_staging_notice_actions(); - - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $this->assertEquals( '', get_option( 'llms_site_url' ) ); - $this->assertTrue( LLMS_Site::is_clone_ignored() ); - $this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) ); - $this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - - update_option( 'siteurl', $original ); - unset( $_SERVER['HTTP_REFERER'] ); - - throw $exception; - } - - } - - /** - * Test the menu_warning() method - * - * @since 4.12.0 - * - * @return void - */ - public function test_menu_warning() { - - $mock_menu = array( - array( - 'Dashboard', - 'read', - 'index.php', - '', - 'menu-top menu-top-first menu-icon-dashboard', - 'menu-dashboard', - 'dashicons-dashboard', - ), - array( - 'Orders', - 'edit_posts', - 'edit.php?post_type=llms_order', - '', - 'menu-top menu-icon-llms_order', - 'menu-posts-llms_order', - 'dashicons-cart', - ), - ); - - global $menu; - $menu = $mock_menu; - - LLMS_Site::update_feature( 'recurring_payments', true ); - LLMS_Staging::menu_warning(); - $this->assertSame( $mock_menu, $menu ); - - - LLMS_Site::update_feature( 'recurring_payments', false ); - LLMS_Staging::menu_warning(); - $this->assertSame( $mock_menu[0], $menu[0] ); - - $mock_menu[1][0] .= LLMS_Unit_Test_Util::call_method( 'LLMS_Staging', 'get_menu_warning_bubble' ); - $this->assertSame( $mock_menu[1], $menu[1] ); - - } - - /** - * Test notice() method - * - * @since 4.12.0 - * - * @return void - */ - public function test_notice() { - - LLMS_Admin_Notices::delete_notice( 'maybe-staging' ); - LLMS_Staging::notice(); - $this->assertTrue( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) ); - LLMS_Admin_Notices::delete_notice( 'maybe-staging' ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-student-query.php b/tests/phpunit/unit-tests/class-llms-test-student-query.php deleted file mode 100644 index 6b7a917f91..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-student-query.php +++ /dev/null @@ -1,198 +0,0 @@ -<?php -/** - * Tests for the LLMS_Install Class - * @group LLMS_Student_Query - * @since 3.3.1 - * @version 3.13.0 - */ -class LLMS_Test_Student_Query extends LLMS_UnitTestCase { - - /** - * Create a new query for use in these tests - * @param array $args args to pass to the query - * @return obj - * @since 3.4.0 - * @version 3.4.0 - */ - private function query( $args = array() ) { - return new LLMS_Student_Query( $args ); - } - - /** - * Test get() and set() functions - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - public function test_getters_setters() { - - $args = array( - 'page' => 2, - 'per_page' => 25, - 'post_id' => 1234, - 'search' => 'a search string', - 'sort' => array( - 'id' => 'ASC', - ), - 'suppress_filters' => true, - 'statuses' => array( - 'enrolled', 'expired' - ), - ); - - $query = $this->query(); - - foreach ( $args as $key => $val ) { - - $query->set( $key, $val ); - $this->assertEquals( $args[ $key ], $query->get( $key ) ); - - // test defaults - unset( $query->query_vars[ $key ] ); - $this->assertEquals( 'default_val', $query->get( $key, 'default_val' ) ); - - } - - } - - /** - * Test some real queries - * @return void - * @since 3.13.0 - * @version 3.13.0 - */ - public function test_get_students() { - - $course_id = $this->generate_mock_courses( 1, 1, 1, 0 )[0]; - - $students = $this->factory->user->create_many( 25, array( 'role' => 'student' ) ); - foreach ( $students as $sid ) { - llms_enroll_student( $sid, $course_id, 'testing' ); - } - - // 25 students enrolled - $query = $this->query( array( - 'post_id' => $course_id, - 'per_page' => 10, - ) ); - - $this->assertEquals( 25, $query->found_results ); - $this->assertEquals( 10, $query->number_results ); - $this->assertEquals( 3, $query->max_pages ); - - sleep( 1 ); // sleep because timestamps can't be the same for the next queries to work correctly - - // unenroll 10 students & results should stay the same - foreach ( $query->get_students() as $student ) { - $student->unenroll( $course_id, 'testing' ); - } - - // check for expired from any courses - $query = $this->query( array( - 'per_page' => 10, - 'statuses' => 'expired', - ) ); - $this->assertEquals( 10, $query->found_results ); - - // check for any status again - $query = $this->query( array( - 'post_id' => $course_id, - 'per_page' => 10, - ) ); - $this->assertEquals( 25, $query->found_results ); - $this->assertEquals( 10, $query->number_results ); - $this->assertEquals( 3, $query->max_pages ); - - // check for enrolled only - $query = $this->query( array( - 'post_id' => $course_id, - 'per_page' => 10, - 'statuses' => 'enrolled', - ) ); - $this->assertEquals( 15, $query->found_results ); - $this->assertEquals( 10, $query->number_results ); - $this->assertEquals( 2, $query->max_pages ); - - - // second course - $course_id2 = $this->generate_mock_courses( 1, 1, 1, 0 )[0]; - $students2 = $this->factory->user->create_many( 25, array( 'role' => 'student' ) ); - foreach ( array_merge( $students, $students2 ) as $sid ) { - llms_enroll_student( $sid, $course_id2, 'testing' ); - } - - // check for enrolled only - $query = $this->query( array( - 'post_id' => array( $course_id, $course_id2 ), - 'per_page' => 10, - // 'statuses' => 'enrolled', - ) ); - $this->assertEquals( 50, $query->found_results ); - $this->assertEquals( 10, $query->number_results ); - $this->assertEquals( 5, $query->max_pages ); - - // more students who aren't enrolled - $students3 = $this->factory->user->create_many( 25, array( 'role' => 'student' ) ); - - // anything in any course - $query = $this->query( array( - 'per_page' => 10, - ) ); - $this->assertEquals( 50, $query->found_results ); - - // cancelled in any course (shouldn't have anything here) - $query = $this->query( array( - 'per_page' => 10, - 'statuses' => 'cancelled', - ) ); - $this->assertEquals( 0, $query->found_results ); - - - // test some searches - $query = $this->query( array( - 'search' => 'No Results Found Plz' - ) ); - $this->assertEquals( 0, $query->found_results ); - - // should hit all the mock users - $query = $this->query( array( - 'search' => 'user_' - ) ); - $this->assertEquals( 50, $query->found_results ); - - - update_user_meta( $students2[5], 'first_name', 'testymcname' ); - $query = $this->query( array( - 'search' => 'testymcname' - ) ); - $this->assertEquals( 1, $query->found_results ); - $this->assertEquals( $students2[5], $query->get_students()[0]->get_id() ); - - } - - /** - * Test the parse_setup_args() function - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - public function test_parse_setup_args() { - - $query = $this->query(); - $this->assertEquals( array_keys( llms_get_enrollment_statuses() ), $query->get( 'statuses' ) ); - - // ensure valid string is converted to array - $query = $this->query( array( 'statuses' => 'enrolled' ) ); - $this->assertEquals( array( 'enrolled' ), $query->get( 'statuses' ) ); - - // ensure invalid status is removed - $query = $this->query( array( 'statuses' => array( 'ooboi', 'enrolled' ) ) ); - $this->assertFalse( in_array( 'ooboi', $query->get( 'statuses' ) ) ); - - // ensure at least one status is returned - $query = $this->query( array( 'statuses' => array( 'ooboi', 'fake' ) ) ); - $this->assertGreaterThanOrEqual( 1, count( $query->get( 'statuses' ) ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-template-functions.php b/tests/phpunit/unit-tests/class-llms-test-template-functions.php deleted file mode 100644 index 52fe10408a..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-template-functions.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * Tests for template functions - * - * @group functions_templates - * - * @since 3.15.0 - * @version 3.37.0 - */ -class LLMS_Functions_Templates extends LLMS_UnitTestCase { - - /** - * Test lifterlms_course_continue_button() func - * - * @since 3.15.0 - * - * @return void - */ - public function test_lifterlms_course_continue_button() { - - global $post; - $func = 'lifterlms_course_continue_button'; - - // student to use - $student = $this->get_mock_student(); - - // course to use - $course_id = $this->generate_mock_courses()[0]; - $course = llms_get_post( $course_id ); - - // blog post to test globals against - $post_id = $this->factory->post->create( array( - 'post_title' => 'Test Post', - ) ); - - - // call function with no parameters (using only defaults) - // no student and no post set right now - $this->assertEmpty( $this->get_output( $func ) ); - - // set the global post to be a blog post - $post = get_post( $post_id ); - - // call function with no parameters (using only defaults) - // post is a blog post & no student - $this->assertEmpty( $this->get_output( $func ) ); - - // set global to be a course but still no student - $post = get_post( $course_id ); - $this->assertEmpty( $this->get_output( $func ) ); - - // set the current student (should display a continue button) - wp_set_current_user( $student->get_id() ); - - // student setup but no enrollment - $this->assertEmpty( $this->get_output( $func ) ); - - // enroll student - llms_enroll_student( $student->get_id(), $course_id ); - - // 0 progress, "Get Started" text displays in button - $this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Get Started' ) ) ); - - // Progress > 0, "Continue" text displays in button - $this->complete_courses_for_student( $student->get_id(), array( $course_id ), 85 ); - $this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Continue' ) ) ); - - // 100% progress, "Course Complete" text displays - $this->complete_courses_for_student( $student->get_id(), array( $course_id ), 100 ); - $this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Course Complete' ) ) ); - - // use a lesson, same result as last - $post = get_post( $course->get_lessons( 'ids' )[0] ); - $this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Course Complete' ) ) ); - - // reset global - $post = null; - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-template-loader.php b/tests/phpunit/unit-tests/class-llms-test-template-loader.php deleted file mode 100644 index f5d840a10c..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-template-loader.php +++ /dev/null @@ -1,398 +0,0 @@ -<?php -/** - * Test LLMS_Template_Loader - * - * @package LifterLMS/Tests - * - * @group template_loader - * - * @since 3.41.1 - */ -class LLMS_Test_Template_Loader extends LLMS_UnitTestCase { - - /** - * Mock restriction id when calling `mock_page_restricted()`. - * - * @var integer - */ - private $mock_page_restricted_id = 987; - - /** - * Setup test case. - * - * @since 3.41.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Template_Loader(); - - } - - /** - * Callback for testing custom restrictions applied through a filter. - * - * @since 3.41.1 - * @since 4.10.1 Use `$this->mock_page_restricted_id` for the restriction_id to allow easy customization of the mocked data. - * - * @param array $restrictions Restriction data array from `llms_page_restricted()`. - * @param int $post_id WP_Post ID. - * @return array - */ - public function mock_page_restricted( $restrictions, $post_id ) { - - $restrictions['is_restricted'] = true; - $restrictions['reason'] = 'mock'; - $restrictions['restriction_id'] = $this->mock_page_restricted_id; - - return $restrictions; - - } - - /** - * Retrieve a WP_Post object for restriction-related tests. - * - * @since 3.41.1 - * - * @param string $post_type Post type to be created. - * @return WP_Post - */ - protected function get_post_for_restrictions( $post_type = 'post' ) { - return $this->factory->post->create_and_get( array( - 'post_type' => $post_type, - 'post_content' => 'content', - 'post_excerpt' => 'excerpt', - ) ); - } - - /** - * Assertion helper for restriction-related tests. - * - * @since 3.41.1 - * - * @param WP_Post $post Post object - * @param string $content Expected post_content string. - * @param string $excerpt Expected post_excerpt string. - * @return void - */ - protected function assertContentEquals( $post, $content = 'content', $excerpt = 'excerpt' ) { - - $this->assertEquals( $content, $post->post_content ); - $this->assertEquals( $excerpt, $post->post_excerpt ); - - } - - /** - * Test maybe_restrict_post_content(): for a skipped post type. - * - * @since 3.41.1 - * - * @return void - */ - public function test_maybe_restrict_post_content_skipped_post_type() { - - global $wp_query; - - $post = $this->get_post_for_restrictions( 'course' ); - - $this->main->maybe_restrict_post_content( $post, $wp_query ); - - $this->assertContentEquals( $post ); - - } - - /** - * Test maybe_restrict_post_content(): for a valid post type that's not restricted - * - * @since 3.41.1 - * - * @return void - */ - public function test_maybe_restrict_post_content_not_restricted() { - - global $wp_query, $post; - - $post = $this->get_post_for_restrictions(); - - $this->main->maybe_restrict_post_content( $post, $wp_query ); - - $this->assertContentEquals( $post ); - - } - - /** - * Test maybe_restrict_post_content(): for a post restricted by a membership (when not accessible by the user) - * - * @since 3.41.1 - * - * @return void - */ - public function test_maybe_restrict_post_content_restricted_by_membership_not_accessible() { - - global $wp_query, $post; - - $membership = llms_get_post( $this->factory->post->create( array( - 'post_type' => 'llms_membership', - ) ) ); - - $membership->set( 'restriction_add_notice', 'yes' ); - $membership->set( 'restriction_notice', 'no access.' ); - - $post = $this->get_post_for_restrictions(); - - update_post_meta( $post->ID, '_llms_restricted_levels', array( $membership->get( 'id' ) ) ); - update_post_meta( $post->ID, '_llms_is_restricted', 'yes' ); - - $this->main->maybe_restrict_post_content( $post, $wp_query ); - - $this->assertContentEquals( $post, 'no access.', 'no access.' ); - - } - - /** - * Test maybe_restrict_post_content(): for a post restricted by a membership that is accessible by the user - * - * @since 3.41.1 - * - * @return void - */ - public function test_maybe_restrict_post_content_restricted_by_membership_is_accessible() { - - global $wp_query, $post; - - $membership = llms_get_post( $this->factory->post->create( array( - 'post_type' => 'llms_membership', - ) ) ); - - $membership->set( 'restriction_add_notice', 'yes' ); - $membership->set( 'restriction_notice', 'no access.' ); - - $post = $this->get_post_for_restrictions(); - - update_post_meta( $post->ID, '_llms_restricted_levels', array( $membership->get( 'id' ) ) ); - update_post_meta( $post->ID, '_llms_is_restricted', 'yes' ); - - $student = $this->get_mock_student(); - $student->enroll( $membership->get( 'id' ) ); - wp_set_current_user( $student->get( 'id' ) ); - - $this->main->maybe_restrict_post_content( $post, $wp_query ); - - $this->assertContentEquals( $post ); - - } - - /** - * Test maybe_restrict_post_content(): for a custom restriction applied via filter by a 3rd party. - * - * @since 3.41.1 - * - * @return void - */ - public function test_maybe_restrict_post_content_restricted_by_other() { - - add_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10, 2 ); - - global $wp_query, $post; - - $post = $this->get_post_for_restrictions(); - - $this->main->maybe_restrict_post_content( $post, $wp_query ); - - $this->assertContentEquals( $post, 'This content is restricted', 'This content is restricted' ); - - remove_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10 ); - - } - - /** - * Test template_loader() with a screen we don't care about modifying - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_default() { - - $this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ); - - } - - /** - * Test template_loader() on the blog (home) page. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_is_home() { - - // Mock `llms_page_restricted()` to have a sitewide membership restriction. - $handler = function( $results ) { - $results['reason'] = 'sitewide_membership'; - $results['is_restricted'] = true; - return $results; - }; - - // Mock `is_home()` so it looks like we're on the blog post page. - global $wp_query; - $temp = $wp_query->is_home; - $wp_query->is_home = true; - - // No restrictions. - $this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ); - $this->assertSame( 0, did_action( 'lifterlms_content_restricted' ) ); - $this->assertSame( 0, did_action( 'llms_content_restricted_by_sitewide_membership' ) ); - $this->assertFalse( has_action( 'loop_start', 'llms_print_notices' ) ); - - // Has restrictions. - add_filter( 'llms_page_restricted', $handler ); - $this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ); - $this->assertSame( 1, did_action( 'lifterlms_content_restricted' ) ); - $this->assertSame( 1, did_action( 'llms_content_restricted_by_sitewide_membership' ) ); - $this->assertEquals( 5, has_action( 'loop_start', 'llms_print_notices' ) ); - - // Reset. - $wp_query->is_home = $temp; - remove_filter( 'llms_page_restricted', $handler ); - - } - - /** - * Test template_loader() for restricted pages. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_page_is_restricted() { - - add_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10, 2 ); - - // Modify the template & fire actions. - $this->assertEquals( 'single-no-access.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - $this->assertSame( 1, did_action( 'lifterlms_content_restricted' ) ); - $this->assertSame( 1, did_action( 'llms_content_restricted_by_mock' ) ); - - // Courses and memberships return the original template (but still fire actions). - global $post; - foreach ( array( 'course', 'llms_membership' ) as $i => $post_type ) { - - $post = $this->factory->post->create_and_get( compact( 'post_type' ) ); - $this->mock_page_restricted_id = $post->ID; - - $this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ); - $this->assertSame( $i + 2, did_action( 'lifterlms_content_restricted' ) ); - $this->assertSame( $i + 2, did_action( 'llms_content_restricted_by_mock' ) ); - - } - - remove_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10 ); - - } - - /** - * Test template_loader() with the course catalog. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_courses() { - - // Post type archive. - $this->go_to( get_post_type_archive_link( 'course' ) ); - $this->assertEquals( 'archive-course.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - - // Check the course catalog page. - LLMS_Install::create_pages(); - $this->go_to( get_permalink( llms_get_page_id( 'courses' ) ) ); - $this->assertEquals( 'archive-course.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - - } - - /** - * Test template_loader() with the membership catalog. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_memberships() { - - // Post type archive. - $this->go_to( get_post_type_archive_link( 'llms_membership' ) ); - $this->assertEquals( 'archive-llms_membership.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - - // Check the membership catalog page. - LLMS_Install::create_pages(); - $this->go_to( get_permalink( llms_get_page_id( 'memberships' ) ) ); - $this->assertEquals( 'archive-llms_membership.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - - } - - /** - * Test template_loader() on custom taxonomy archives. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_for_taxonomies() { - - foreach ( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) as $tax ) { - - $term = wp_create_term( 'mock-' . $tax, $tax ); - $this->go_to( get_term_link( $term['term_id'] ) ); - // $this->assertTrue( is_course_taxonomy() ); - $this->assertEquals( sprintf( 'taxonomy-%s.php', $tax ), basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) ); - - } - - } - - /** - * Test template_loader() on certificate pages. - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_certificates() { - - global $post, $wp_query; - foreach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) { - - $post = $this->factory->post->create_and_get( compact( 'post_type' ) ); - $wp_query->queried_object = $post; - $wp_query->is_singular = true; - $this->assertEquals( 'single-certificate.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ), $post_type ); - - } - - } - - /** - * Test template_loader() with a default unrestricted post type - * - * @since 4.10.1 - * - * @return void - */ - public function test_template_loader_default_post_type() { - - global $post; - $post = $this->factory->post->create_and_get(); - - // Not touched. - $this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ); - $this->assertSame( 0, did_action( 'lifterlms_content_restricted' ) ); - $this->assertSame( 0, did_action( 'llms_content_restricted_by_sitewide_membership' ) ); - $this->assertFalse( has_action( 'loop_start', 'llms_print_notices' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/class-llms-test-view-manager.php b/tests/phpunit/unit-tests/class-llms-test-view-manager.php deleted file mode 100644 index cc4bc9b1a3..0000000000 --- a/tests/phpunit/unit-tests/class-llms-test-view-manager.php +++ /dev/null @@ -1,496 +0,0 @@ -<?php -/** - * Test view manager - * - * @package LifterLMS/Tests - * - * @group view_manager - * - * @since 4.5.1 - */ -class LLMS_Test_View_Manager extends LLMS_UnitTestCase { - - /** - * Setup test case - * - * @since 4.5.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_View_Manager(); - - } - - /** - * Initiate (and retrieve) an instance of WP_Admin_Bar - * - * @since 4.16.0 - * - * @return WP_Admin_Bar - */ - private function get_admin_bar() { - - add_filter( 'show_admin_bar', '__return_true' ); - _wp_admin_bar_init(); - - global $wp_admin_bar; - - remove_filter( 'show_admin_bar', '__return_true' ); - - return $wp_admin_bar; - - } - - /** - * Mock `$_GET` data to control the return of `get_view()`. - * - * @since 4.16.0 - * - * @param string $role Requested view role. - * @return void - */ - public function mock_view_data( $role ) { - - $this->mockGetRequest( array( - 'view_nonce' => wp_create_nonce( 'llms-view-as' ), - 'llms-view-as' => $role, - ) ); - - } - - /** - * Test constructor - * - * @since 4.5.1 - * - * @return void - */ - public function test__construct() { - - // Remove existing action. - remove_action( 'init', array( $this->main, 'add_actions' ) ); - $this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) ); - - // Reinit. - $this->main = new LLMS_View_Manager(); - $this->assertEquals( 10, has_action( 'init', array( $this->main, 'add_actions' ) ) ); - - } - - /** - * Test constructor when a pending order is being created. - * - * @since 4.5.1 - * - * @return void - */ - public function test__construct_pending_order() { - - // Remove existing action. - remove_action( 'init', array( $this->main, 'add_actions' ) ); - $this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) ); - - // Reinit. - $this->mockPostRequest( array( - 'action' => 'create_pending_order', - ) ); - $this->main = new LLMS_View_Manager(); - $this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) ); - } - - /** - * Test add_menu_items() when the display manager shouldn't be displayed. - * - * @since 4.16.0 - * - * @return void - */ - public function test_add_menu_items_no_display() { - - $bar = $this->get_admin_bar(); - - $this->main->add_menu_items( $bar ); - - $this->assertNull( $bar->get_nodes() ); - - } - - /** - * Test add_menu_items() - * - * @since 4.16.0 - * - * @return void - */ - public function test_add_menu_items() { - - $bar = $this->get_admin_bar(); - - add_filter( 'llms_view_manager_should_display', '__return_true' ); - - $this->main->add_menu_items( $bar ); - - $this->assertEquals( array( 'llms-view-as-menu', 'llms-view-as--visitor', 'llms-view-as--student' ), array_keys( $bar->get_nodes() ) ); - - remove_filter( 'llms_view_manager_should_display', '__return_true' ); - - } - - /** - * Test get_url() with a supplied URL and additional QS args. - * - * @since 4.16.0 - * - * @return void - */ - public function test_get_url_with_url() { - - $url = parse_url( LLMS_View_Manager::get_url( 'visitor', 'https://mock.tld/test?whatever=0', array( 'more' => 'yes' ) ) ); - - // Make sure URL is preserved properly. - $this->assertEquals( 'https', $url['scheme'] ); - $this->assertEquals( 'mock.tld', $url['host'] ); - $this->assertEquals( '/test', $url['path'] ); - - // Check query vars. - parse_str( $url['query'], $qs ); - $this->assertEquals( '0', $qs['whatever'] ); - $this->assertEquals( 'yes', $qs['more'] ); - $this->assertEquals( 'visitor', $qs['llms-view-as'] ); - - // Ensure generated nonce is valid. - $this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) ); - - } - - /** - * Test get_url() with a supplied URL and additional QS args. - * - * @since 4.16.0 - * - * @return void - */ - public function test_get_url_without_url() { - - $_SERVER['REQUEST_URI'] = 'https://fake.tld'; - - $url = parse_url( LLMS_View_Manager::get_url( 'student' ) ); - - // Make sure URL is preserved properly. - $this->assertEquals( 'https', $url['scheme'] ); - $this->assertEquals( 'fake.tld', $url['host'] ); - - // Check query vars. - parse_str( $url['query'], $qs ); - $this->assertEquals( 'student', $qs['llms-view-as'] ); - - // Ensure generated nonce is valid. - $this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) ); - - $_SERVER['REQUEST_URI'] = ''; - - } - - /** - * Test get_view() when there's nonce errors. - * - * @since 4.16.0 - * - * @return void - */ - public function test_get_view_nonce_error() { - - // Nothing set. - $this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) ); - - // Invalid nonce. - $this->mockGetRequest( array( - 'view_nonce' => 'fake', - 'llms-view-as' => 'student', - ) ); - $this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) ); - - } - - /** - * Test get_view() with an invalid view. - * - * @since 4.16.0 - * - * @return void - */ - public function test_get_view_invalid_view() { - - $this->mock_view_data( 'fake' ); - $this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) ); - - } - - /** - * Test get_view() with valid data. - * - * @since 4.16.0 - * - * @return void - */ - public function test_get_view() { - - foreach ( array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_views' ) ) as $view ) { - - $this->mock_view_data( $view ); - $this->assertEquals( $view, LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) ); - - } - - } - - /** - * Test modify_dashboard() - * - * @since 4.16.0 - * - * @return void - */ - public function test_modify_dashboard() { - - // Unchanged when viewing as self. - $this->assertNull( $this->main->modify_dashboard( null ) ); - - // Visitors can't load the dashboard (they see forms). - $this->mock_view_data( 'visitor' ); - $this->assertFalse( $this->main->modify_dashboard( null ) ); - - // Students see the dashboard. - $this->mock_view_data( 'student' ); - $this->assertTrue( $this->main->modify_dashboard( null ) ); - - } - - /** - * Test modify_course_open(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_modify_course_open() { - - $course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - - $this->mock_view_data( 'visitor' ); - $this->assertFalse( $this->main->modify_course_open( false, $course ) ); - - // Logged out user. - $this->mock_view_data( 'self' ); - $this->assertFalse( $this->main->modify_course_open( false, $course ) ); - - // Admin can do it. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertTrue( $this->main->modify_course_open( false, $course ) ); - - // Instructor can't. - $instructor = $this->factory->user->create( array( 'role' => 'instructor' ) ); - wp_set_current_user( $instructor ); - $this->assertFalse( $this->main->modify_course_open( false, $course ) ); - - $course->set_instructors( array( - array( - 'id' => $instructor, - ) - ) ); - $this->assertTrue( $this->main->modify_course_open( false, $course ) ); - - } - - /** - * Test modify_restrictions(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_modify_restrictions() { - - $course = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $mock_restriction = array( - 'content_id' => $course, - 'is_restricted' => true, - 'reason' => 'enrollment_course', - 'restriction_id' => $course, - ); - - $expected_success = wp_parse_args( array( - 'is_restricted' => false, - 'reason' => 'role-access', - ), $mock_restriction ); - - $this->mock_view_data( 'visitor' ); - $this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) ); - - // No user. - $this->mock_view_data( 'self' ); - $this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertEquals( $expected_success, $this->main->modify_restrictions( $mock_restriction ) ); - - $instructor = $this->factory->user->create( array( 'role' => 'instructor' ) ); - wp_set_current_user( $instructor ); - $this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) ); - - llms_get_post( $course )->set_instructors( array( - array( - 'id' => $instructor, - ) - ) ); - - $this->assertEquals( $expected_success, $this->main->modify_restrictions( $mock_restriction ) ); - - } - - /** - * Test should_display() when viewing valid post types with a valid user - * - * @since 4.5.1 - * @since 5.9.0 Add tests for instructors. - * - * @return void - */ - public function test_should_display_on_valid_post_types() { - - global $post; - - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - $instructor = $this->factory->user->create( array( 'role' => 'instructor' ) ); - - foreach ( array( 'course', 'lesson', 'llms_membership', 'llms_quiz' ) as $post_type ) { - - wp_set_current_user( $admin ); - - $post = $this->factory->post->create_and_get( compact( 'post_type' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - wp_set_current_user( $instructor ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - if ( in_array( $post_type, array( 'course', 'llms_membership' ), true ) ) { - llms_get_post( $post )->set_instructors( array( - array( - 'id' => $instructor, - ) - ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - } - - } - - } - - /** - * Test should_display() when viewing checkout valid with a valid user - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_on_checkout() { - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'checkout' ) ); - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - } - - /** - * Test should_display() when viewing the student dashboard with a valid user - * - * @since 4.16.0 - * - * @return void - */ - public function test_should_display_on_dashboard() { - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'myaccount' ) ); - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - } - - /** - * Test should_display() when no user is present - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_no_user() { - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - } - - /** - * Test should_display() when an invalid user is logged in - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_invalid_user() { - wp_set_current_user( $this->factory->user->create( array( 'role' => 'student' ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - } - - /** - * Test should_display() on the admin panel - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_in_admin() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - set_current_screen( 'users.php' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - set_current_screen( 'front' ); // Reset for later tests. - - } - - /** - * Test should_display() on a post type archive - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_post_type_archive() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->go_to( get_post_type_archive_link( 'course' ) ); - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - } - - /** - * Test should_display() when a valid using is viewing an invalid post type - * - * @since 4.5.1 - * - * @return void - */ - public function test_should_display_on_invalid_post_types() { - - global $post; - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - foreach ( array( 'post', 'page' ) as $post_type ) { - $post = $this->factory->post->create_and_get( compact( 'post_type' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-conroller-quizzes.php b/tests/phpunit/unit-tests/controllers/class-llms-test-conroller-quizzes.php deleted file mode 100644 index 358d22d4ab..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-conroller-quizzes.php +++ /dev/null @@ -1,177 +0,0 @@ -<?php -/** - * Test LLMS_Controller_Quizzes - * - * @package LifterLMS/Tests/Controllers - * - * @group controllers - * @group quizzes - * @group controller_quizzes - * - * @since 3.37.8 - */ -class LLMS_Test_Controller_Quizzes extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 3.37.8 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->controller = new LLMS_Controller_Quizzes(); - - } - - /** - * Test maybe_handle_reporting_actions(): form not submitted - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_not_submitted() { - - $this->assertNull( $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions(): invalid nonce - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_invalid_nonce() { - - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => 'fake', - ) ); - - $this->assertNull( $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions(): there's no quiz id passed via to the button form element. - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_no_button() { - - // Button not set. - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - ) ); - - $this->assertFalse( $this->controller->maybe_handle_reporting_actions() ); - - // Button empty - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - 'llms_del_quiz' => '', - ) ); - - $this->assertFalse( $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions(): submitted WP Post ID isn't a quiz id. - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_not_a_quiz() { - - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - 'llms_del_quiz' => $this->factory->post->create(), - ) ); - - $this->assertFalse( $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions(): the quiz isn't an orphan. - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_not_an_orphan() { - - $courses = $this->generate_mock_courses( 1, 1, 1, 1, 1 ); - $lesson = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] ); - $quiz = $lesson->get_quiz(); - - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - 'llms_del_quiz' => $quiz->get( 'id' ), - ) ); - - $this->assertFalse( $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions() success: the quiz is an orphan and can be deleted. - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_is_orphan() { - - $quiz = $this->factory->post->create_and_get( array( 'post_type' => 'llms_quiz' ) ); - - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - 'llms_del_quiz' => $quiz->ID, - ) ); - - $this->assertEquals( $quiz, $this->controller->maybe_handle_reporting_actions() ); - - } - - /** - * Test maybe_handle_reporting_actions() success: the quiz's parent course doesn't exist anymore and the quiz can be deleted. - * - * @since 3.37.8 - * - * @return void - */ - public function test_maybe_handle_reporting_actions_no_course() { - - $courses = $this->generate_mock_courses( 1, 1, 1, 1, 1 ); - $lesson = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] ); - $quiz = $lesson->get_quiz(); - - $this->mockPostRequest( array( - '_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ), - 'llms_del_quiz' => $quiz->get( 'id' ), - ) ); - - // Now it's attached to an orphaned lesson, we should still be able to delete it. - $lesson->set( 'parent_course', '' ); - - $this->assertEquals( $quiz->post, $this->controller->maybe_handle_reporting_actions() ); - - } - - - - - - - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-account.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-account.php deleted file mode 100644 index 41fe0e8db6..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-account.php +++ /dev/null @@ -1,1087 +0,0 @@ -<?php -/** - * Tests for the LLMS_Controller_Account class - * - * @group controllers - * @group controller_account - * - * @since 3.19.0 - * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib. - * @since 3.37.17 Added tests for the `lost_password()` and `reset_password()` methods. - * @since 4.12.0 Added tests for `redeem_voucher()` method. - */ -class LLMS_Test_Controller_Account extends LLMS_UnitTestCase { - - // Consider dates equal within 60 seconds. - private $date_delta = 60; - - /** - * Setup the test case. - * - * @since 3.37.17 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Controller_Account(); - - } - - /** - * Teardown the test case. - * - * Clears LifterLMS Notices. - * - * @since 3.37.17 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - llms_clear_notices(); - - } - - /** - * Mock wp_mail() arguments to ensure we fail when we want to test a wp_mail() failure. - * - * @since 3.37.17 - * - * @param array $args Associative array of arguments passed to wp_mail() - * @return array - */ - public function fail_wp_mail( $args ) { - - $args['to'] = 'fail'; - return $args; - - } - - /** - * Test order completion actions - * - * @since 3.19.0 - * @since 3.37.17 Use `$this->main->cancel_subscription()` instead of `do_action( 'init' )`. - * - * @return void - */ - public function test_cancel_subscription() { - - // form not submitted - $this->setup_post( array() ); - $this->main->cancel_subscription(); - $this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) ); - - // form submitted but missing required fields - $this->setup_post( array( - '_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ), - ) ); - $this->main->cancel_subscription(); - $this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) ); - $this->assertEquals( 1, llms_notice_count( 'error' ) ); - - llms_clear_notices(); - - // form submitted but invalid order id or the order id is invalid - $this->setup_post( array( - '_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ), - 'order_id' => 123, - ) ); - $this->main->cancel_subscription(); - $this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) ); - $this->assertEquals( 1, llms_notice_count( 'error' ) ); - - llms_clear_notices(); - - // create a real order - $order = $this->get_mock_order(); - - // form submitted but invalid order id or the order doesn't belong to the current user - $this->setup_post( array( - '_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ), - 'order_id' => $order->get( 'id' ), - ) ); - $this->main->cancel_subscription(); - $this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) ); - $this->assertEquals( 1, llms_notice_count( 'error' ) ); - - llms_clear_notices(); - wp_set_current_user( $order->get( 'user_id' ) ); - - foreach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) { - - // active order moves to pending cancel - $order->set_status( $status ); - - $this->setup_post( array( - '_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ), - 'order_id' => $order->get( 'id' ), - ) ); - $this->main->cancel_subscription(); - - $expected = 'llms-active' === $status ? 'llms-pending-cancel' : 'llms-cancelled'; - $this->assertEquals( $expected, get_post_status( $order->get( 'id' ) ) ); - - } - - } - - /** - * Test lost_password() when form not submitted. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_not_submitted() { - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->assertNull( $this->main->lost_password() ); - $this->assertEquals( $actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - } - - /** - * Test lost_password() when an invalid nonce is submitted. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_invalid_nonce() { - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => 'fake', - ) ); - - $this->assertNull( $this->main->lost_password() ); - $this->assertEquals( $actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - } - - /** - * Test the lost password form returns an error if missing a required field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_lost_password_missing_required() { - - $controller = new LLMS_Controller_Account(); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - ) ); - $res = $controller->lost_password(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_pass_reset_missing_login', $res ); - - $this->assertEquals( 1, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertStringContains( 'Enter a username or e-mail address.', llms_get_notices() ); - - } - - /** - * Test lost_password() error: login not submitted. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_missing_login() { - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - ) ); - - $res = $this->main->lost_password(); - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_pass_reset_missing_login', $res ); - - $this->assertHasNotice( 'Enter a username or e-mail address.', 'error' ); - - } - - /** - * Test lost_password() error: user not found. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_user_not_found_email() { - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => 'fake', - ) ); - - $res = $this->main->lost_password(); - - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_pass_reset_invalid_login', $res ); - - $this->assertHasNotice( 'Invalid username or e-mail address.', 'error' ); - - } - - /** - * Test lost_password() returns errors for an invalid username. - * - * @since 5.0.0 - * - * @return vod - */ - public function test_lost_password_user_not_found_email_username() { - - $controller = new LLMS_Controller_Account(); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => 'thisisafakeusername', - ) ); - - $res = $controller->lost_password(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_pass_reset_invalid_login', $res ); - - $this->assertStringContains( 'Invalid username or e-mail address.', llms_get_notices() ); - - } - - /** - * Test lost_password() when WP core get_password_reset_key() returns an error or password reset is disabled via filters. - * - * @since 5.0.0 - * - * @return void - */ - public function test_lost_password_key_error() { - - $controller = new LLMS_Controller_Account(); - - $user = $this->factory->user->create_and_get(); - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->user_login, - ) ); - - // Mock an error. - add_filter( 'allow_password_reset', '__return_false' ); - - $res = $controller->lost_password(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'no_password_reset', $res ); - - $this->assertStringContains( 'Password reset is not allowed for this user', llms_get_notices() ); - - remove_filter( 'allow_password_reset', '__return_false' ); - - } - - /** - * Test lost_password() success. - * - * @since 5.0.0 - * - * @return void - */ - public function test_lost_password_email_success() { - - // Something prior to this test triggers a password changed email to be sent and causes this test to fail as a result. - // Adding a reset here is faster than tracking down the test that causes that email to be sent. - reset_phpmailer_instance(); - - $controller = new LLMS_Controller_Account(); - - $user = $this->factory->user->create_and_get(); - - // Test with user-submitted email & username. - foreach ( array( 'user_email', 'user_login' ) as $field ) { - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->$field, - ) ); - - $this->assertTrue( $controller->lost_password() ); - - $this->assertStringContains( 'Check your e-mail for the confirmation link.', llms_get_notices() ); - - // Test the email sent. - $sent = tests_retrieve_phpmailer_instance()->get_sent(); - $this->assertEquals( $user->user_email, $sent->to[0][0] ); - $this->assertEquals( 'Password Reset for Test Blog', $sent->subject ); - - } - - reset_phpmailer_instance(); - - } - - /** - * Test lost_password() when password reset is disabled by the `allow_password_reset` WP core filter. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_reset_disabled() { - - $user = $this->factory->user->create_and_get(); - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->user_email, - ) ); - - add_filter( 'allow_password_reset', '__return_false' ); - - $res = $this->main->lost_password(); - - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'no_password_reset', $res ); - - $this->assertHasNotice( 'Password reset is not allowed for this user', 'error' ); - - remove_filter( 'allow_password_reset', '__return_false' ); - - } - - /** - * Test lost_password() when a wp_mail() error is encountered. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_email_error() { - - $user = $this->factory->user->create_and_get(); - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->user_email, - ) ); - - add_filter( 'wp_mail', array( $this, 'fail_wp_mail' ) ); - - $res = $this->main->lost_password(); - - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_pass_reset_email_failure', $res ); - - $this->assertHasNotice( 'Unable to reset password due to an unknown error. Please try again.', 'error' ); - - remove_filter( 'wp_mail', array( $this, 'fail_wp_mail' ) ); - - } - - /** - * Test lost_password() success with an email address. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_with_email_success() { - - $user = $this->factory->user->create_and_get(); - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->user_email, - ) ); - - $res = $this->main->lost_password(); - - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertTrue( $res ); - - $this->assertHasNotice( 'Check your e-mail for the confirmation link.', 'success' ); - - } - - /** - * Test lost_password() success with username. - * - * @since 3.37.17 - * - * @return void - */ - public function test_lost_password_with_login_success() { - - $user = $this->factory->user->create_and_get(); - - // Baseline actions count. - $actions = did_action( 'llms_before_lost_password_form_submit' ); - - $this->mockPostRequest( array( - '_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ), - 'llms_login' => $user->user_login, - ) ); - - $res = $this->main->lost_password(); - - $this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) ); - - $this->assertTrue( $res ); - - $this->assertHasNotice( 'Check your e-mail for the confirmation link.', 'success' ); - - } - - /** - * Test redeem_voucher() when the form isn't submitted - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_not_submitted() { - $this->assertNull( $this->main->redeem_voucher() ); - } - - /** - * Test redeem_voucher() when there's an invalid nonce - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_invalid_nonce() { - - $this->mockPostRequest( array( - 'lifterlms_voucher_nonce' => 'fake', - ) ); - - $this->assertNull( $this->main->redeem_voucher() ); - - } - - /** - * Test redeem_voucher() when no voucher code is submitted - * - * Note: the error message doesn't really make sense but in real world scenarios - * and end user will never encounter this error as HTML5 validation prevents - * the form from being submitted without a voucher. - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_missing_voucher() { - - wp_set_current_user( $this->factory->user->create() ); - $this->mockPostRequest( array( - 'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ), - ) ); - - $res = $this->main->redeem_voucher(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'not-found', $res ); - $this->assertHasNotice( 'Voucher code "" could not be found.', 'error' ); - - } - - /** - * Test redeem_voucher() when there's no user - * - * This shouldn't ever really happen but we'll test it just in case. - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_missing_user() { - - $this->mockPostRequest( array( - 'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ), - ) ); - - $this->assertNull( $this->main->redeem_voucher() ); - - } - - /** - * Test redeem_voucher() when an error is encountered during the voucher redemption - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_error() { - - wp_set_current_user( $this->factory->user->create() ); - $this->mockPostRequest( array( - 'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ), - 'llms_voucher_code' => 'fakevouchercode1', - ) ); - - $res = $this->main->redeem_voucher(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'not-found', $res ); - $this->assertHasNotice( 'Voucher code "fakevouchercode1" could not be found.', 'error' ); - - } - - /** - * Test redeem_voucher() success - * - * @since 4.12.0 - * - * @return void - */ - public function test_redeem_voucher_success() { - - $voucher = $this->create_voucher( 1, 1 ); - - wp_set_current_user( $this->factory->user->create() ); - $this->mockPostRequest( array( - 'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ), - 'llms_voucher_code' => $voucher->get_voucher_codes()[0]->code, - ) ); - - $this->assertTrue( $this->main->redeem_voucher() ); - $this->assertHasNotice( 'Voucher redeemed successfully!', 'success' ); - - } - - /** - * Test reset_password(): form not submitted. - * - * @since 3.37.17 - * - * @return void - */ - public function test_reset_password_not_submitted() { - - $this->assertNull( $this->main->reset_password() ); - - } - - /** - * Test reset_password(): invalid nonce - * - * @since 3.37.17 - * - * @return void - */ - public function test_reset_password_invalid_nonce() { - - $this->mockPostRequest( array( - '_reset_password_nonce' => 'fake', - ) ); - - $this->assertNull( $this->main->reset_password() ); - - } - - /** - * Test reset_password(): form validation errors (missing required fields) - * - * @since 3.37.17 - * - * @return void - */ - public function test_reset_password_form_validation_error() { - - $this->mockPostRequest( array( - '_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ), - ) ); - - $res = $this->main->reset_password(); - - $this->assertIsWPError( $res ); - $this->assertEquals( 1, count( $res->errors ) ); - - $errors = array( - 'Password is a required field', - 'Confirm Password is a required field', - ); - - $notices = llms_get_notices(); - - foreach ( $errors as $error ) { - $this->assertStringContains( $error, $notices ); - } - - } - - /** - * Test reset_password(): password reset key errors - * - * @since 3.37.17 - * - * @return void - */ - public function test_reset_password_reset_key_errors() { - - $pass = wp_generate_password( 12 ); - - // Fake user and key. - $post = array( - '_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ), - 'password' => $pass, - 'password_confirm' => $pass, - 'llms_reset_key' => 'fake', - 'llms_reset_login' => 'fake', - ); - $this->mockPostRequest( $post ); - - $res = $this->main->reset_password(); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res ); - $this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() ); - - // Real user fake key. - $user = $this->factory->user->create_and_get(); - $data['llms_reset_login'] = $user->user_login; - $this->mockPostRequest( $post ); - - $res = $this->main->reset_password(); - - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res ); - $this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() ); - - } - - /** - * Test reset_password() submitted passwords don't match. - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_no_match() { - - $controller = new LLMS_Controller_Account(); - - $this->mockPostRequest( array( - '_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ), - 'password' => 'fake', - 'password_confirm' => 'fake2', - ) ); - - $res = $controller->reset_password(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms-passwords-must-match', $res ); - - $notices = llms_get_notices(); - $this->assertStringContains( 'The submitted passwords do must match.', $notices ); - - } - - /** - * Test reset_password() with an expired password reset key. - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_expired_key() { - - add_filter( 'password_reset_expiration', '__return_zero' ); - - $controller = new LLMS_Controller_Account(); - - $user = $this->factory->user->create_and_get(); - $key = get_password_reset_key( $user ); - - llms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) ); - - $this->mockPostRequest( array( - '_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ), - 'password' => 'fake', - 'password_confirm' => 'fake', - 'llms_reset_login' => $user->user_login, - 'llms_reset_key' => $key, - ) ); - - $res = $controller->reset_password(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_expired_key', $res ); - $this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() ); - - remove_filter( 'password_reset_expiration', '__return_zero' ); - - } - - /** - * Test reset_password() success - * - * @since 3.37.17 - * @since 4.21.0 Added more assertions for testing with special character passwords. - * - * @return void - */ - public function test_reset_password_success() { - - LLMS_Install::create_pages(); - $controller = new LLMS_Controller_Account(); - - $passwords = array( - // See notes on spaces below. - ' with leading space', - 'with trailing space ', - - // Some simple characters. - '123456arst!', - '\slashy \ passwordy', - '<such>()-!=***. special!y320', - - // These passwords were failing before we improved the tests. - ' AxwVr=@D`z5SXh&Cj/z{#8xta>rvx Nr!5Ur48rtI[ykmc8k~Uj&HO>)/$4:z98', - 'G!*EODpN[!rw] Z|tW|L4.2]@Iok1b1ws(kF3~BP0B%_}./{?)5y$Y`ODn|#-!x ', - ' w,~5E3`=RiPZq&.q0P>-R]1t|t7Qxev', - '_rsBES.Icg~T)c( -UPh?;Dhu>Up{|b!woR{_hynn7$0(*e1mI1Q3t9(h.V1h]v ', - - // Generate some random passwords to test. - wp_generate_password( 12 ), - wp_generate_password( 32, true, true ), - wp_generate_password( 64, true, true ), - ); - - foreach ( $passwords as $pass ) { - - wp_set_current_user( null ); - - $user = $this->factory->user->create_and_get(); - - // Fake user and key. - $post = array( - '_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ), - 'password' => $pass, - 'password_confirm' => $pass, - 'llms_reset_key' => get_password_reset_key( $user ), - 'llms_reset_login' => $user->user_login, - ); - - $this->mockPostRequest( $post ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'reset_password_handler' ) ); - - $user = get_user_by( 'id', $user->ID ); - - /** - * Because of `wp_magic_quotes()`, slashes will be automatically added when a user actually tries to login. - * - * We also will trim the password because WP runs `trim()` on passwords when logging in / creating accounts - * but it doesn't run it when using `wp_check_password()` itself: https://core.trac.wordpress.org/ticket/34889. - * - * We'll add these to `wp_check_password()` here to make sure that a user can actually login with their newly updated - * password. - */ - $this->assertTrue( wp_check_password( addslashes( trim( $pass ) ), $user->user_pass ), $pass ); - - // $this->assertHasNotices( 'success' ); - // $this->assertStringContains( 'Your password has been updated.', llms_get_notices() ); - - // User should be able to login using our login functionality / forms. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => $user->user_email, - /** - * Here we add slashes to simulate a physical $_POST with `wp_magic_quotes()` but we don't need to `trim()` because - * that's handled in `wp_authenticate()` (called by `wp_signon()` used by our handler). - */ - 'llms_password' => addslashes( $pass ), - ) ); - - $this->assertEquals( $user->ID, $login, $pass ); - - wp_set_current_user( null ); - - // Authenticate via the WP core method directly (redundant but...). - $auth = wp_authenticate( $user->user_login, addslashes( $pass ) ); - $this->assertEquals( $auth, $user, $pass ); - - } - - } - - /** - * Test reset_password_link_redirect(): no redirect when not on the account page. - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_link_redirect_not_account_page() { - - $controller = new LLMS_Controller_Account(); - $this->go_to( home_url() ); - - $controller->reset_password_link_redirect(); - $this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) ); - - } - - /** - * Test reset_password_link_redirect(): no redirect when missing key and/or login params. - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_link_redirect_no_vars() { - - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'myaccount' ) ); - - $controller = new LLMS_Controller_Account(); - - // No vars. - $controller->reset_password_link_redirect(); - $this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) ); - - // No login. - $this->mockGetRequest( array( - 'key' => 'fake-key', - ) ); - $controller->reset_password_link_redirect(); - $this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) ); - - // No key. - $this->mockGetRequest( array( - 'login' => 'fake-login', - ) ); - $controller->reset_password_link_redirect(); - $this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) ); - - } - - /** - * Test reset_password_link_redirect(): redirect & set the cookie (even if it's an invalid user.) - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_link_redirect_success_fake_user() { - - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'myaccount' ) ); - - $controller = new LLMS_Controller_Account(); - $this->mockGetRequest( array( - 'key' => 'fake-key', - 'login' => 'fake-login', - ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( add_query_arg( 'reset-pass', 1, llms_lostpassword_url() ) . ' [302] YES' ); - - try { - - $controller->reset_password_link_redirect(); - - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $cookie = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertEquals( '0:fake-key', $cookie['value'] ); - throw $exception; - - } - - } - - /** - * Test reset_password_link_redirect(): redirect & set the cookie with a valid user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_reset_password_link_redirect_success_real_user() { - - LLMS_Install::create_pages(); - $this->go_to( llms_get_page_url( 'myaccount' ) ); - $user = $this->factory->user->create_and_get(); - - $controller = new LLMS_Controller_Account(); - $this->mockGetRequest( array( - 'key' => 'fake-key', - 'login' => $user->user_login, - ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( add_query_arg( 'reset-pass', 1, llms_lostpassword_url() ) . ' [302] YES' ); - - try { - - $controller->reset_password_link_redirect(); - - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $cookie = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertEquals( sprintf( '%d:fake-key', $user->ID ), $cookie['value'] ); - throw $exception; - - } - - } - - /** - * Test account update form submission handler when form is not submitted - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_not_submitted() { - - $this->mockPostRequest( array() ); - $this->main->update(); - $this->assertEquals( 0, did_action( 'llms_before_user_account_update_submit' ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) ); - - } - - /** - * Test account update form submission handler when user is not logged in - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_no_user() { - - // form submitted but user isn't logged in - $this->mockPostRequest( array( - '_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ), - ) ); - $this->main->update(); - $this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) ); - llms_clear_notices(); - - } - - /** - * Test account update form submission handler when missing required fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_missing_fields() { - - LLMS_Install::create_pages(); - LLMS_Forms::instance()->install( true ); - - // create a user - $uid = $this->factory->user->create(); - // sign the user in - wp_set_current_user( $uid ); - - // form submitted but missing fields - $this->mockPostRequest( array( - '_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ), - ) ); - $this->main->update(); - $this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) ); - llms_clear_notices(); - - } - - /** - * Test account update form submission handler - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_success() { - - LLMS_Install::create_pages(); - LLMS_Forms::instance()->install(); - - // I can't figure out why the action in the constructor isn't added when this test is run. - LLMS_Unit_Test_Util::call_method( LLMS_Form_Handler::instance(), '__construct' ); - - // create a user - $uid = $this->factory->user->create(); - // sign the user in - wp_set_current_user( $uid ); - - // update something - $this->mockPostRequest( array( - '_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ), - 'email_address' => 'help+23568@lifterlms.com', - 'email_address_confirm' => 'help+23568@lifterlms.com', - 'display_name' => 'Marshall P.', - 'first_name' => 'Marshall', - 'last_name' => 'Pate', - 'llms_billing_address_1' => 'Voluptatem', - 'llms_billing_address_2' => '#12345', - 'llms_billing_city' => 'Harum est dolorum sed vel perspiciatis consequatur dignissimos possimus delectus quos optio omnis error quas rem dicta et consectetur odio', - 'llms_billing_state' => 'Esse ea est dolore sed sunt ipsum a ut nemo dolorem aut aliquam cillum asperiores minim culpa', - 'llms_billing_zip' => '72995', - 'llms_billing_country' => 'US', - ) ); - - // exceptions thrown in testing env instead of exit() - $this->expectException( LLMS_Unit_Test_Exception_Exit::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_endpoint_url( 'edit-account', '', llms_get_page_url( 'myaccount' ) ) ) ); - - // run these assertions within actions because the exit() at the end of the redirect will halt program execution - // and then we'll never get to these assertions! - add_action( 'llms_before_user_account_update_submit', function() { - $this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) ); - $this->assertEquals( 0, llms_notice_count( 'error' ) ); - } ); - add_action( 'lifterlms_user_updated', function() { - $this->assertEquals( 1, did_action( 'lifterlms_user_updated' ) ); - } ); - - $this->main->update(); - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-certificates.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-certificates.php deleted file mode 100644 index 5efc6ffe08..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-certificates.php +++ /dev/null @@ -1,308 +0,0 @@ -<?php -/** - * Test LLMS_Controller_Certificates - * - * @package LifterLMS/Tests/Controllers - * - * @group controllers - * @group certificates - * @group controller_certificates - * - * @since 3.37.4 - * @since 4.5.0 Add tests for managing certificate sharing settings. - */ -class LLMS_Test_Controller_Certificates extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 3.37.4 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->instance = new LLMS_Controller_Certificates(); - - } - - /** - * Test maybe_allow_public_query(): no authorization data in query string. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_allow_public_query_no_auth() { - $this->assertEquals( array(), $this->instance->maybe_allow_public_query( array() ) ); - } - - /** - * Test maybe_allow_public_query(): authorization present but invalid. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_allow_public_query_invalid_auth() { - - // Doesn't exist. - $args = array( - 'publicly_queryable' => false, - ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'fake', - ) ); - - $this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) ); - - // Post exists but submitted nocne is incorrect. - $post_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) ); - update_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'incorrect-nonce', - ) ); - $this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) ); - - } - - /** - * Test maybe_allow_public_query(): authorization present and exists but on an invalid post type. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_allow_public_query_invalid_post_type() { - - $post_id = $this->factory->post->create(); - update_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'mock-nonce', - ) ); - - $args = array( - 'publicly_queryable' => false, - ); - - $this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) ); - - } - - /** - * Test maybe_allow_public_query(): valid auth and post type. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_allow_public_query_update() { - - $post_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) ); - update_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'mock-nonce', - ) ); - - $args = array( - 'publicly_queryable' => false, - ); - $expect = array( - 'publicly_queryable' => true, - ); - - $this->assertEquals( $expect, $this->instance->maybe_allow_public_query( $args ) ); - - } - - /** - * Test maybe_authenticate_export_generation() when no authorization data is passed. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_authenticate_export_generation_no_auth() { - - $this->instance->maybe_authenticate_export_generation(); - $this->assertEquals( 0, get_current_user_id() ); - - } - - /** - * Test maybe_authenticate_export_generation() when no authorization data is passed. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_authenticate_export_generation_invalid_post_type() { - - global $post; - $temp = $post; - $post = $this->factory->post->create_and_get(); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'fake', - ) ); - - $this->instance->maybe_authenticate_export_generation(); - $this->assertEquals( 0, get_current_user_id() ); - - // Reset post. - $post = $temp; - - } - - /** - * Test maybe_authenticate_export_generation() when no authorization data is passed. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_authenticate_export_generation_invalid_nonce() { - - foreach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) { - - global $post; - $temp = $post; - $post = $this->factory->post->create_and_get( array( 'post_type' => $post_type ) ); - - update_post_meta( $post->ID, '_llms_auth_nonce', 'mock-nonce' ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'fake', - ) ); - - $this->instance->maybe_authenticate_export_generation(); - $this->assertEquals( 0, get_current_user_id() ); - - // Reset post. - $post = $temp; - - } - - } - - /** - * Test maybe_authenticate_export_generation() for a certificate template. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_authenticate_export_generation_for_template() { - - $uid = $this->factory->user->create( array( 'role' => 'lms_manager' ) ); - - $template = $this->create_certificate_template(); - update_post_meta( $template, '_llms_auth_nonce', 'mock-nonce' ); - wp_update_post( array( - 'ID' => $template, - 'post_author' => $uid, - ) ); - - global $post; - $temp = $post; - $post = get_post( $template ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'mock-nonce', - ) ); - - $this->instance->maybe_authenticate_export_generation(); - $this->assertEquals( $uid, get_current_user_id() ); - - // Reset post. - $post = $temp; - - } - - /** - * Test maybe_authenticate_export_generation() for an earned certificate. - * - * @since 3.37.4 - * - * @return void - */ - public function test_maybe_authenticate_export_generation_for_earned_cert() { - - $uid = $this->factory->student->create(); - - $template = $this->create_certificate_template(); - - $earned = $this->earn_certificate( $uid, $template, $this->factory->post->create() ); - - global $post; - $temp = $post; - $post = get_post( $earned[1] ); - update_post_meta( $post->ID, '_llms_auth_nonce', 'mock-nonce' ); - - $this->mockGetRequest( array( - '_llms_cert_auth' => 'mock-nonce', - ) ); - - $this->instance->maybe_authenticate_export_generation(); - $this->assertEquals( $uid, get_current_user_id() ); - - // Reset post. - $post = $temp; - - } - - /** - * Test change_sharing_settings() when user has insufficient permissions - * - * @since 4.5.0 - * - * @return void - */ - public function test_change_sharing_settings_invalid_permissions() { - - $earned = $this->earn_certificate( $this->factory->student->create(), $this->create_certificate_template(), $this->factory->post->create() ); - - $res = LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $earned[1], true ) ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'insufficient-permissions', $res ); - - } - - /** - * Test change_sharing_settings() - * - * @since 4.5.0 - * - * @return void - */ - public function test_change_sharing_settings() { - - $uid = $this->factory->student->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $this->factory->post->create() ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - wp_set_current_user( $uid ); - - // Enable Sharing - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, true ) ) ); - $this->assertEquals( 'yes', $cert->get( 'allow_sharing' ) ); - - // Already enabled. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, true ) ) ); - $this->assertEquals( 'yes', $cert->get( 'allow_sharing' ) ); - - // Disable sharing. - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, false ) ) ); - $this->assertEquals( 'no', $cert->get( 'allow_sharing' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-lesson-progression.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-lesson-progression.php deleted file mode 100644 index 5360d861f9..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-lesson-progression.php +++ /dev/null @@ -1,208 +0,0 @@ -<?php -/** - * Tests for LifterLMS Lesson Progression Forms & Functions - * - * @group controllers - * @group lessons - * - * @since 3.17.1 - */ -class LLMS_Test_Controller_Lesson_Progression extends LLMS_UnitTestCase { - - /** - * Setup tests. - * - * @since 3.17.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - llms_clear_notices(); - parent::set_up(); - } - - /** - * Test the handle_admin_managment_forms() method. - * - * @since 3.29.0 - * - * @return void - */ - public function test_handle_admin_managment_forms() { - - $data = array(); - - $class = new LLMS_Controller_Lesson_Progression(); - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 2, 'quizzes' => 0 ) ); - $student_id = $this->factory->student->create_and_enroll( $course->get( 'id' ) ); - - // Form not submitted. - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 0, did_action( 'llms_mark_complete' ) ); - - // Form submitted but missing required fields. - $data['llms-admin-progression-nonce'] = wp_create_nonce( 'llms-admin-lesson-progression' ); - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 0, did_action( 'llms_mark_complete' ) ); - - $data['lesson_id'] = $course->get_lessons( 'ids' )[0]; - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 0, did_action( 'llms_mark_complete' ) ); - - $data['student_id'] = $student_id; - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 0, did_action( 'llms_mark_complete' ) ); - - // All data but invalid action.. - $data['llms-lesson-action'] = 'fake'; - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 0, did_action( 'llms_mark_complete' ) ); - - // Mark the lesson complete.. - $data['llms-lesson-action'] = 'complete'; - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 1, did_action( 'llms_mark_complete' ) ); - - // Mark it incomplete.. - $data['llms-lesson-action'] = 'incomplete'; - $this->mockPostRequest( $data ); - $class->handle_admin_managment_forms(); - $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.. - $this->assertEquals( 1, did_action( 'llms_mark_complete' ) ); - - } - - /** - * Test the submission of the mark lesson complete form - * - * @since 3.17.1 - * @since 3.29.0 Unknown. - * - * @return void - */ - public function test_handle_complete_form() { - - // Form not submitted. - $this->mockPostRequest( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) ); - - // Form submitted but missing required fields. - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_complete' ), - ) ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) ); - - // Form submitted but invalid lesson id. - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_complete' ), - 'mark-complete' => 'wut', // Lesson id. - 'mark_complete' => '', // Button. - ) ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) ); - $this->assertEquals( 1, llms_notice_count( 'error' ) ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson_id = $course->get_lessons( 'ids' )[0]; - - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - wp_set_current_user( $student->get_id() ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_complete' ), - 'mark-complete' => $lesson_id, // Lesson id. - 'mark_complete' => '', // Button. - ) ); - do_action( 'init' ); - $this->assertEquals( 1, did_action( 'llms_trigger_lesson_completion' ) ); - $this->assertTrue( $student->is_complete( $lesson_id, 'lesson' ) ); - - } - - /** - * Test the submission of the mark lesson incomplete form - * - * @since 3.17.1 - * @since 3.29.0 Unknown. - * - * @return void - */ - public function test_handle_incomplete_form() { - - // Form not submitted. - $this->mockPostRequest( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - - // Form submitted but missing required fields. - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_incomplete' ), - ) ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - - // Form submitted but invalid lesson id. - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_incomplete' ), - 'mark-incomplete' => 'wut', // Lesson id. - 'mark_incomplete' => '', // Button. - ) ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) ); - $this->assertEquals( 1, llms_notice_count( 'error' ) ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson_id = $course->get_lessons( 'ids' )[0]; - - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - $student->mark_complete( $lesson_id, 'lesson' ); - wp_set_current_user( $student->get_id() ); - - $this->mockPostRequest( array( - '_wpnonce' => wp_create_nonce( 'mark_incomplete' ), - 'mark-incomplete' => $lesson_id, // Lesson id. - 'mark_incomplete' => '', // Button. - ) ); - do_action( 'init' ); - $this->assertFalse( $student->is_complete( $lesson_id, 'lesson' ) ); - - } - - /** - * Test the Mark Complete function as triggered by the `llms_trigger_lesson_completion` action - * - * @since 3.17.1 - * - * @return void - */ - public function test_mark_complete() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson_id = $course->get_lessons( 'ids' )[0]; - - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - - do_action( 'llms_trigger_lesson_completion', $student->get( 'id' ), $lesson_id ); - $this->assertTrue( $student->is_complete( $lesson_id, 'lesson' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-login.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-login.php deleted file mode 100644 index 6842e4ac09..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-login.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php -/** - * Tests for the LLMS_Controller_Registration class - * - * @group controllers - * @group registration - * - * @since 3.19.4 - * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib. - */ -class LLMS_Test_Controller_Login extends LLMS_UnitTestCase { - - /** - * Test order completion actions - * - * @since 3.19.4 - * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib. - * - * @return void - */ - public function test_login() { - - LLMS_Install::create_pages(); - - // form not submitted - $this->setup_post( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'lifterlms_before_user_login' ) ); - $this->assertEquals( 0, did_action( 'wp_login' ) ); - - // not submitted - $this->setup_get( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'lifterlms_before_user_login' ) ); - $this->assertEquals( 0, did_action( 'wp_login' ) ); - - // form submitted but missing things - $this->setup_post( array( - '_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ), - ) ); - do_action( 'init' ); - $this->assertEquals( 1, did_action( 'lifterlms_before_user_login' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'wp_login' ) ); - llms_clear_notices(); - - // incomplete form - $this->setup_post( array( - '_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ), - 'email_address' => 'fake@mock.org', - ) ); - do_action( 'init' ); - $this->assertEquals( 2, did_action( 'lifterlms_before_user_login' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'wp_login' ) ); - llms_clear_notices(); - - $uid = $this->factory->user->create( array( - 'user_email' => 'test@arstarst.com', - 'user_pass' => '123456789', - ) ); - - // this should login a user - $this->setup_post( array( - '_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ), - 'llms_login' => 'test@arstarst.com', - 'llms_password' => '123456789', - ) ); - - // exceptions thrown in testing env instead of exit() - $this->expectException( LLMS_Unit_Test_Exception_Exit::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_page_url( 'myaccount' ) ) ); - - // run these assertions within actions because the exit() at the end of the redirect will halt program execution - // and then we'll never get to these assertions! - add_action( 'lifterlms_before_user_login', function() { - $this->assertEquals( 3, did_action( 'lifterlms_before_user_login' ) ); - $this->assertEquals( 0, llms_notice_count( 'error' ) ); - } ); - add_action( 'wp_login', function( $login, $user ) use ( $uid ) { - $this->assertEquals( $uid, $user->ID ); - $this->assertEquals( 1, did_action( 'wp_login' ) ); - wp_logout(); - }, 10, 2 ); - - do_action( 'init' ); - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-orders.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-orders.php deleted file mode 100644 index 564414e317..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-orders.php +++ /dev/null @@ -1,701 +0,0 @@ -<?php -/** - * Tests for the LLMS_Controller_Orders class. - * - * @package LifterLMS/Tests - * - * @group orders - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 3.33.0 Add test for the `on_delete_order` method. - * @since 3.36.1 When testing deleting/erroring orders make sure to schedule a recurring payment when setting an order as active so that, - * when subsequently we error/delete the order, checking the recurring payment is unscheduled makes sense. - * Also add tests on recurrint payments not processed when order or user deleted. - * @since 4.2.0 Added `test_on_user_enrollment_deleted()`. - * @since 5.4.0 Added test on recurring_charge attempts on orders when related product manually removed. - */ -class LLMS_Test_Controller_Orders extends LLMS_UnitTestCase { - - // Consider dates equal within 60 seconds. - private $date_delta = 60; - - /** - * Setup the test case - * - * @since Unknown - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - LLMS_Site::update_feature( 'recurring_payments', true ); - - } - - /** - * Disable manual gateway recurring payments for mocking error conditions. - * - * @since 3.32.0 - * - * @param array $supports Gateway features array. - * @param string $gateway_id Gateway ID. - * @return array - */ - public function mod_gateway_features( $supports, $gateway_id ) { - - if ( 'manual' === $gateway_id ) { - $supports['recurring_payments'] = false; - } - - return $supports; - - } - - /** - * Test order completion actions. - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of `assertEquals()` with 4th parameter. - * - * @return void - */ - public function test_complete_order() { - - /** - * Tests for one-time payment with no access expiration - */ - $plan = $this->get_mock_plan( '25.99', 0 ); - $order = $this->get_mock_order( $plan ); - - // Student not yet enrolled. - $this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Complete the order. - $order->set( 'status', 'llms-completed' ); - - // Student gets enrolled. - $this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Student now has lifetime access. - $this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() ); - - // No next payment date. - $this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) ); - - // Actions were run. - $this->assertEquals( 1, did_action( 'lifterlms_product_purchased' ) ); - $this->assertEquals( 1, did_action( 'lifterlms_access_plan_purchased' ) ); - - /** - * Tests for one-time payment with access expiration - */ - $plan = $this->get_mock_plan( '25.99', 0, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - - // Student not yet enrolled. - $this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Complete the order. - $order->set( 'status', 'llms-completed' ); - - // Student gets enrolled. - $this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Student will expire based on expiration settings. - $this->assertEquals( date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS ), $order->get_access_expiration_date() ); - - // No next payment date. - $this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) ); - - // Actions were run. - $this->assertEquals( 2, did_action( 'lifterlms_product_purchased' ) ); - $this->assertEquals( 2, did_action( 'lifterlms_access_plan_purchased' ) ); - - /** - * Tests for recurring payment - */ - $plan = $this->get_mock_plan( '25.99', 1 ); - $order = $this->get_mock_order( $plan ); - - // Student not yet enrolled. - $this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Complete the order. - $order->set( 'status', 'llms-active' ); - - // Student gets enrolled. - $this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Student now has lifetime access. - $this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() ); - - // Next payment date. - $this->assertEqualsWithDelta( (float) date( 'U', current_time( 'timestamp' ) + DAY_IN_SECONDS ), (float) $order->get_next_payment_due_date( 'U' ), $this->date_delta ); - - // Actions were run. - $this->assertEquals( 3, did_action( 'lifterlms_product_purchased' ) ); - $this->assertEquals( 3, did_action( 'lifterlms_access_plan_purchased' ) ); - - // Cancel the order to test reactivation. - $this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() ); - $order->set( 'status', 'llms-pending-cancel' ); - $order->set( 'status', 'llms-active' ); - - // Should still have lifetime access after reactivation. - $this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() ); - - // Expiration event should be cleared. - $this->assertFalse( as_next_scheduled_action( 'llms_access_plan_expiration', array( - 'order_id' => $order->get( 'id' ), - ) ) ); - - // Test a limited date order for reactivation events. - $plan = $this->get_mock_plan( '25.99', 1, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - $order->set( 'status', 'llms-pending-cancel' ); - $order->set( 'status', 'llms-active' ); - $this->assertEquals( date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS ), $order->get_access_expiration_date( 'Y-m-d' ) ); - - // Expiration event should be reset. - $this->assertEqualsWithDelta( - (float) $order->get_access_expiration_date( 'U' ), - (float) as_next_scheduled_action( - 'llms_access_plan_expiration', - array( - 'order_id' => $order->get( 'id' ), - ) - ), $this->date_delta ); - - } - - /** - * Test order error statuses. - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 3.36.1 Make sure to schedule a recurring payment when setting an order as active so that, - * when subsequently we error the order, checking the recurring payment is unscheduled makes sense. - * @since 5.2.0 Test upcoming payment reminder. - * - * @return void - */ - public function test_error_order() { - - $err_statuses = array( - 'llms-refunded' => 'cancelled', - 'llms-cancelled' => 'cancelled', - 'llms-expired' => 'expired', - 'llms-failed' => 'expired', - 'llms-on-hold' => 'cancelled', - 'llms-trash' => 'cancelled', - ); - - foreach ( $err_statuses as $status => $enrollment_status ) { - - $order = $this->get_mock_order(); - - $student = llms_get_student( $order->get( 'user_id' ) ); - - // Schedule payments & enroll the student. - $order->set( 'status', 'llms-active' ); - - $order->maybe_schedule_payment(); - - // Recurring payment is scheduled. - $this->assertEquals( - $order->get_next_payment_due_date( 'U' ), - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Error the order. - $order->set( 'status', $status ); - - // Student should be removed. - $this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) ); - - // Status should be changed. - $this->assertEquals( $enrollment_status, $student->get_enrollment_status( $order->get( 'product_id' ) ) ); - - // Recurring payment is unscheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Upcoming payment reminder is unscheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - } - - } - - /** - * Test delete order. - * - * @since 3.33.0 - * @since 3.36.1 Check recurring payment is unscheduled. - * @since 5.2.0 Test upcoming payment reminder. - * - * @return void - */ - public function test_on_delete_order() { - - $order = $this->get_mock_order(); - $student = llms_get_student( $order->get( 'user_id' ) ); - - $order_product_id = $order->get( 'product_id' ); - - // Schedule payments & enroll the student. - $order->set( 'status', 'llms-active' ); - - $order->maybe_schedule_payment(); - - // Recurring payment is scheduled. - $this->assertEquals( - $order->get_next_payment_due_date( 'U' ), - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Delete order. - wp_delete_post( $order->get( 'id' ), false ); - - // Student should be removed. - $this->assertFalse( $student->is_enrolled( $order_product_id ) ); - - // More in depth checks. - // Enrollment status must be false. - $this->assertFalse( $student->get_enrollment_status( $order_product_id ) ); - - // Enrollment trigger must be false. - $this->assertFalse( $student->get_enrollment_trigger( $order_product_id ) ); - - // Enrollment date must be false. - $this->assertFalse( $student->get_enrollment_date( $order_product_id ) ); - - // Recurring payment is unscheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Upcoming payment reminder is unscheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - } - - /** - * Test on user enrollment deleted. - * - * The controller's `on_user_enrollment_deleted()` method is reponsible of changing the order status to `cancelled` - * in reaction to the deletion of an enrollment with the same order as trigger. - * - * @since 4.2.0 - * - * @return void - */ - public function test_on_user_enrollment_deleted() { - - $order = $this->get_mock_order(); - $student_id = $order->get( 'user_id' ); - $order_product_id = $order->get( 'product_id' ); - $order_id = $order->get( 'id' ); - - // Enroll the student. - $order->set( 'status', 'llms-active' ); - - $order_cancelled_actions = did_action( 'lifterlms_order_status_cancelled' ); - - $fake_order_id = $order_id + 999; - - // Delete user enrollment passing a fake order as trigger. - llms_delete_student_enrollment( $student_id, $order_product_id, "order_{$fake_order_id}" ); - $this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) ); - // Check order status. - $this->assertEquals( 'llms-active', llms_get_post( $order_id )->get( 'status' ) ); - - // Delete user enrollment. - llms_delete_student_enrollment( $student_id, $order_product_id, "order_{$order_id}" ); - $this->assertEquals( $order_cancelled_actions + 1, did_action( 'lifterlms_order_status_cancelled' ) ); - // Check order status. - $this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) ); - - $order_cancelled_actions = did_action( 'lifterlms_order_status_cancelled' ); - - // Check that trying to delete it again doesn't trigger the action again. - llms_delete_student_enrollment( $student_id, $order_product_id, "order_{$order_id}" ); - $this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) ); - // Check order status. - $this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) ); - - // Enroll the student again on the same course with a different trigger. - $student = llms_get_student( $student_id ); - llms_enroll_student( $student_id, $order_product_id ); - - llms_delete_student_enrollment( $student_id, $order_product_id, "order_{$order_id}" ); - $this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) ); - // Check order status. - $this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) ); - - } - - /** - * Test expire access function. - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 5.2.0 Test upcoming payment reminder. - * - * @return void - */ - public function test_expire_access() { - - // Recurring -> expire via access settings. - $plan = $this->get_mock_plan( '25.99', 1, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - $order->set_status( 'active' ); - $student = llms_get_student( $order->get( 'user_id' ) ); - - do_action( 'llms_access_plan_expiration', $order->get( 'id' ) ); - - $this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) ); - - // Recurring payment is not scheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Upcoming payment reminder is not scheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - $this->assertEquals( 'expired', $student->get_enrollment_status( $order->get( 'product_id' ) ) ); - $this->assertEquals( 'llms-active', $order->get( 'status' ) ); - - // Simulate a pending-cancel -> cancel. - $plan = $this->get_mock_plan( '25.99', 1, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - $order->set_status( 'active' ); - $order->set_status( 'pending-cancel' ); - $student = llms_get_student( $order->get( 'user_id' ) ); - - do_action( 'llms_access_plan_expiration', $order->get( 'id' ) ); - - $this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) ); - - // Recurring payment is not scheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Upcoming payment reminder is not scheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - $this->assertEquals( 'cancelled', $student->get_enrollment_status( $order->get( 'product_id' ) ) ); - $this->assertEquals( 'llms-cancelled', get_post_status( $order->get( 'id' ) ) ); - - } - - /** - * Test recurring_charge attempts on orders manually removed from the database. - * - * @since 3.36.1 - * - * @return void - */ - public function test_recurring_charge_on_manually_deleted_order() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - $order_id = $order->get( 'id' ); - - // Starting action numbers. - $note_actions = did_action( 'llms_new_order_note_added' ); - $err_gw_actions = did_action( 'llms_order_recurring_charge_gateway_error' ); - $pdue_actions = did_action( 'llms_manual_payment_due' ); - $err_order_actions = did_action( 'llms_order_recurring_charge_gateway_error' ); - $err_user_actions = did_action( 'llms_order_recurring_charge_user_error' ); - - // Emulate a manul order deletion from the db. - global $wpdb; - $wpdb->delete( $wpdb->prefix . 'posts', array( 'id' => $order_id ) ); - clean_post_cache( $order_id ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order_id ); - - $this->assertSame( $pdue_actions, did_action( 'llms_manual_payment_due' ) ); - $this->assertSame( $note_actions, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $err_gw_actions, did_action( 'llms_order_recurring_charge_gateway_error' ) ); - $this->assertSame( $err_order_actions + 1, did_action( 'llms_order_recurring_charge_order_error' ) ); - $this->assertSame( $err_user_actions, did_action( 'llms_order_recurring_charge_user_error' ) ); - - } - - - /** - * Test recurring_charge attempts on orders whose user has been deleted. - * - * @since 3.36.1 - * - * @return void - */ - public function test_recurring_charge_on_deleted_user() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - $order_id = $order->get( 'id' ); - - // Starting action numbers. - $note_actions = did_action( 'llms_new_order_note_added' ); - $err_gw_actions = did_action( 'llms_order_recurring_charge_gateway_error' ); - $pdue_actions = did_action( 'llms_manual_payment_due' ); - $err_order_actions = did_action( 'llms_order_recurring_charge_gateway_error' ); - $err_user_actions = did_action( 'llms_order_recurring_charge_user_error' ); - - // Emulate an user deletion. - wp_delete_user( $order->get( 'user_id' ) ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order_id ); - - $this->assertSame( $pdue_actions, did_action( 'llms_manual_payment_due' ) ); - $this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $err_gw_actions, did_action( 'llms_order_recurring_charge_gateway_error' ) ); - $this->assertSame( $err_order_actions, did_action( 'llms_order_recurring_charge_order_error' ) ); - $this->assertSame( $err_user_actions + 1, did_action( 'llms_order_recurring_charge_user_error' ) ); - - } - - /** - * Test gateway-related errors encountered during a recurring_charge attempt. - * - * @since 3.32.0 - * - * @return void - */ - public function test_recurring_charge_gateway_errors() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - - $order->set( 'payment_gateway', 'fake-gateway' ); - - // Starting action numbers. - $note_actions = did_action( 'llms_new_order_note_added' ); - $err_actions = did_action( 'llms_order_recurring_charge_gateway_error' ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order->get( 'id' ) ); - - $this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_gateway_error' ) ); - - } - - /** - * Test a recurring payment processed when recurring payments are disabled on the site. - * - * @since 3.32.0 - * - * @return void - */ - public function test_recurring_charge_staging_mode() { - - // Disable recurring payments. - LLMS_Site::update_feature( 'recurring_payments', false ); - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - - // Starting action numbers. - $skip_actions = did_action( 'llms_order_recurring_charge_skipped' ); - $note_actions = did_action( 'llms_new_order_note_added' ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order->get( 'id' ) ); - - $this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $skip_actions + 1, did_action( 'llms_order_recurring_charge_skipped' ) ); - - } - - /** - * Test gateway-related errors encountered during a recurring_charge attempt. - * - * @since 3.32.0 - * - * @return void - */ - public function test_recurring_charge_gateway_support_disabled() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - - // Disable recurring payments. - add_filter( 'llms_get_gateway_supported_features', array( $this, 'mod_gateway_features' ), 10, 2 ); - - // Starting action numbers. - $err_actions = did_action( 'llms_order_recurring_charge_gateway_payments_disabled' ); - $note_actions = did_action( 'llms_new_order_note_added' ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order->get( 'id' ) ); - - $this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_gateway_payments_disabled' ) ); - - // Re-enable recurring payments. - remove_filter( 'llms_get_gateway_supported_features', array( $this, 'mod_gateway_features' ), 10, 2 ); - - } - - /** - * Test recurring_charge attempts on orders when related product manually removed. - * - * @since 5.4.0 - * - * @return void - */ - public function test_recurring_charge_on_manually_deleted_product() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - - $student = llms_get_student( $order->get( 'user_id' ) ); - - // Schedule payments & enroll the student. - $order->set( 'status', 'llms-active' ); - - // Recurring payment is scheduled. - $this->assertEquals( - $order->get_next_payment_due_date( 'U' ), - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Student gets enrolled. - $this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - // Manually remove product. - global $wpdb; - $wpdb->delete( - $wpdb->posts, - array( - 'ID' => $order->get( 'product_id' ), - ), - array( - '%d', - ) - ); - clean_post_cache( $order->get( 'product_id' ) ); - - // Starting action numbers. - $err_actions = did_action( 'llms_order_recurring_charge_aborted_product_deleted' ); - $note_actions = did_action( 'llms_new_order_note_added' ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order->get( 'id' ) ); - - $this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) ); - $this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_aborted_product_deleted' ) ); - - // Recurring payment is unscheduled. - $this->assertFalse( - as_next_scheduled_action( - 'llms_charge_recurring_payment', - array( - 'order_id' => $order->get( 'id' ), - ) - ) - ); - - // Student unenrolled. - $this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ); - - } - - /** - * Test gateway-related errors encountered during a recurring_charge attempt. - * - * @since 3.32.0 - * - * @return void - */ - public function test_recurring_charge_success() { - - $plan = $this->get_mock_plan( '200.00', 1 ); - $order = $this->get_mock_order( $plan ); - - // Starting action numbers. - $actions = did_action( 'llms_manual_payment_due' ); - - // Trigger recurring payment. - do_action( 'llms_charge_recurring_payment', $order->get( 'id' ) ); - - $this->assertSame( $actions + 1, did_action( 'llms_manual_payment_due' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-registration.php b/tests/phpunit/unit-tests/controllers/class-llms-test-controller-registration.php deleted file mode 100644 index 001df54a88..0000000000 --- a/tests/phpunit/unit-tests/controllers/class-llms-test-controller-registration.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php -/** - * Tests for the LLMS_Controller_Registration class - * - * @group controllers - * @group registration - * @group controller_registration - * - * @since 3.19.4 - * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib. - * @since 5.0.0 Install forms during setup. - */ -class LLMS_Test_Controller_Registration extends LLMS_UnitTestCase { - - /** - * Test registration form submission. - * - * @since 3.19.4 - * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib. - * @since 5.0.0 Install forms during setup. - * - * @return void - */ - public function test_register() { - - LLMS_Install::create_pages(); - LLMS_Forms::instance()->install( true ); - - // form not submitted - $this->setup_post( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) ); - - // not submitted - $this->setup_get( array() ); - do_action( 'init' ); - $this->assertEquals( 0, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) ); - - // form submitted but missing things - $this->setup_post( array( - '_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ), - ) ); - do_action( 'init' ); - $this->assertEquals( 1, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) ); - llms_clear_notices(); - - // user already logged in - $uid = $this->factory->user->create(); - wp_set_current_user( $uid ); - // form submitted but missing things - $this->setup_post( array( - '_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ), - ) ); - do_action( 'init' ); - $this->assertEquals( 2, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) ); - llms_clear_notices(); - - // log that user out - wp_set_current_user( null ); - - // incomplete form - $this->setup_post( array( - '_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ), - 'user_login' => '', - 'email_address' => 'fake@mock.org', - 'password' => 'owb2g1pICH82', - ) ); - do_action( 'init' ); - $this->assertEquals( 3, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) ); - $this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) ); - llms_clear_notices(); - - // this should register a user - $this->setup_post( array( - '_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ), - 'user_login' => '', - 'email_address' => 'fake@mock.org', - 'email_address_confirm' => 'fake@mock.org', - 'password' => 'owb2g1pICH82', - 'password_confirm' => 'owb2g1pICH82', - 'first_name' => 'David', - 'last_name' => 'Stevens', - 'llms_billing_address_1' => 'Voluptatem', - 'llms_billing_address_2' => '#12345', - 'llms_billing_city' => 'Harum est dolorum sed vel perspiciatis consequatur dignissimos possimus delectus quos optio omnis error quas rem dicta et consectetur odio', - 'llms_billing_state' => 'Esse ea est dolore sed sunt ipsum a ut nemo dolorem aut aliquam cillum asperiores minim culpa', - 'llms_billing_zip' => '72995', - 'llms_billing_country' => 'US', - 'llms_voucher' => '', - 'llms_mc_consent' => 'yes', - 'llms_agree_to_terms' => 'yes', - ) ); - - // exceptions thrown in testing env instead of exit() - $this->expectException( LLMS_Unit_Test_Exception_Exit::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_page_url( 'myaccount' ) ) ); - - // run these assertions within actions because the exit() at the end of the redirect will halt program execution - // and then we'll never get to these assertions! - add_action( 'lifterlms_before_new_user_registration', function() { - $this->assertEquals( 4, did_action( 'lifterlms_before_new_user_registration' ) ); - $this->assertEquals( 0, llms_notice_count( 'error' ) ); - } ); - add_action( 'lifterlms_user_registered', function() { - $this->assertEquals( 1, did_action( 'lifterlms_user_registered' ) ); - } ); - - do_action( 'init' ); - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-form-field.php b/tests/phpunit/unit-tests/forms/class-llms-test-form-field.php deleted file mode 100644 index d00b8b915e..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-form-field.php +++ /dev/null @@ -1,1169 +0,0 @@ -<?php -/** - * Test LLMS_Form_Field class - * - * @package LifterLMS/Tests - * - * @group form_field - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Form_Field extends LLMS_Unit_Test_Case { - - /** - * Retrive a new user with specified user meta data. - * - * @since 5.0.0 - * - * @param string $meta_key Meta key name. - * @param string $meta_val Meta value (optional). - * @return int WP_User ID. - */ - private function get_user_with_meta( $meta_key, $meta_val = '' ) { - - $uid = $this->factory->user->create(); - update_user_meta( $uid, $meta_key, $meta_val ); - - wp_set_current_user( $uid ); - - return $uid; - - } - - /** - * teardown the test case. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - wp_set_current_user( null ); - - } - - /** - * Test output of a hidden input field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_hidden() { - - $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 ) ); - - } - - /** - * Test output of a select field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_select() { - - $opts = array( - 'type' => 'select', - 'options' => array( - 'mock' => 'MOCK', - 'fake' => 'FAKE', - ), - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<select class="llms-field-select', $html ); - $this->assertStringContains( '<option value="mock">MOCK</option>', $html ); - $this->assertStringContains( '<option value="fake">FAKE</option>', $html ); - - // With selected value. - $opts['selected'] = 'fake'; - $html = llms_form_field( $opts, false ); - $this->assertStringContains( '<option value="fake" selected="selected">FAKE</option>', $html ); - - unset( $opts['selected'] ); - - // With default value. - $opts['default'] = 'fake'; - $html = llms_form_field( $opts, false ); - $this->assertStringContains( '<option value="fake" selected="selected">FAKE</option>', $html ); - - } - - /** - * Test select field with user data. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_select_with_user_data() { - - $opts = array( - 'type' => 'select', - 'data_store_key' => 'select_data', - 'selected' => 'mock', - 'options' => array( - 'mock' => 'MOCK', - 'fake' => 'FAKE', - ), - ); - - // Uses default value. - $html = llms_form_field( $opts, false ); - $this->assertStringContains( '<option value="mock" selected="selected">MOCK</option>', $html ); - $this->assertStringNotContains( '<option value="fake" selected="selected">FAKE</option>', $html ); - - // No meta saved for user, uses default. - $this->get_user_with_meta( 'other', '' ); - $html = llms_form_field( $opts, false ); - $this->assertStringContains( '<option value="mock" selected="selected">MOCK</option>', $html ); - $this->assertStringNotContains( '<option value="fake" selected="selected">FAKE</option>', $html ); - - // Use user's value. - $this->get_user_with_meta( 'select_data', 'fake' ); - $html = llms_form_field( $opts, false ); - $this->assertStringNotContains( '<option value="mock" selected="selected">MOCK</option>', $html ); - $this->assertStringContains( '<option value="fake" selected="selected">FAKE</option>', $html ); - - } - - /** - * Test select field with an option group. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_select_opt_group() { - - $opts = array( - 'type' => 'select', - 'data_store_key' => 'select_data', - 'options' => array( - array( - 'label' => __( 'Group 1', 'lifterlms' ), - 'options' => array( - 'opt1' => __( 'Option 1', 'lifterlms' ), - 'opt2' => __( 'Option 2', 'lifterlms' ), - ), - ), - array( - 'label' => __( 'Group 2', 'lifterlms' ), - 'options' => array( - 'opt3' => __( 'Option 3', 'lifterlms' ), - 'opt4' => __( 'Option 4', 'lifterlms' ), - ), - ), - ), - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<optgroup label="Group 1" data-key="0">', $html ); - $this->assertStringContains( '<optgroup label="Group 2" data-key="1">', $html ); - - for ( $i = 1; $i <= 4; $i++ ) { - $this->assertStringContains( sprintf( '<option value="opt%1$d">Option %1$d</option>', $i ), $html ); - } - - } - - /** - * Test radio field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_radio() { - - $opts = array( - 'type' => 'radio', - 'value' => 'mock_val', - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<div class="llms-form-field type-radio', $html ); - $this->assertStringContains( '<input class="llms-field-radio"', $html ); - $this->assertStringContains( 'type="radio"', $html ); - $this->assertStringContains( 'value="mock_val"', $html ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - // checked. - $opts['checked'] = true; - $html = llms_form_field( $opts, false ); - $this->assertStringContains( 'checked="checked"', $html ); - - } - - /** - * Test radio field with a user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_radio_with_user() { - - $opts = array( - 'id' => 'radio_store', - 'type' => 'radio', - 'value' => 'mock_val', - ); - - // User doesn't have value stored. - $this->get_user_with_meta( 'radio_store' ); - $html = llms_form_field( $opts, false ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - $this->get_user_with_meta( 'radio_store', 'mock_val' ); - $html = llms_form_field( $opts, false ); - $this->assertStringContains( 'checked="checked"', $html ); - - } - - /** - * Test a radio group field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_radio_group() { - - $opts = array( - 'id' => 'radio-id', - 'label' => 'Radio Label', - 'type' => 'radio', - 'options' => array( - 'opt1' => 'Option1', - 'opt2' => 'Option2', - ), - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<div class="llms-form-field type-radio is-group', $html ); - $this->assertStringContains( '<label for="radio-id">Radio Label</label><div class="llms-field-radio llms-input-group"', $html ); - $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 ); - $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 ); - - // default value. - $opts['default'] = 'opt1'; - $html = llms_form_field( $opts, false ); - $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 ); - - // user has saved data. - $this->get_user_with_meta( 'radio-id', 'opt2' ); - $html = llms_form_field( $opts, false ); - $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 ); - $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 ); - - } - - /** - * Test a checkbox field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_checkbox() { - - $opts = array( - 'type' => 'checkbox', - 'value' => 'mock_val', - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<div class="llms-form-field type-checkbox', $html ); - $this->assertStringContains( '<input class="llms-field-checkbox"', $html ); - $this->assertStringContains( 'type="checkbox"', $html ); - $this->assertStringContains( 'value="mock_val"', $html ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - // Checked. - $opts['checked'] = true; - $html = llms_form_field( $opts, false ); - $this->assertStringContains( 'checked="checked"', $html ); - - } - - /** - * Test checkbox with a user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_checkbox_with_user() { - - $opts = array( - 'id' => 'checkbox_store', - 'type' => 'checkbox', - 'value' => 'mock_val', - ); - - // User doesn't have value stored. - $this->get_user_with_meta( 'checkbox_store' ); - $html = llms_form_field( $opts, false ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - $this->get_user_with_meta( 'checkbox_store', 'mock_val' ); - $html = llms_form_field( $opts, false ); - $this->assertStringContains( 'checked="checked"', $html ); - - } - - /** - * Test checkbox group. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_checkbox_group() { - - $opts = array( - 'id' => 'checkbox-id', - 'label' => 'Checkbox Label', - 'type' => 'checkbox', - 'options' => array( - 'opt1' => 'Option1', - 'opt2' => 'Option2', - ), - ); - - $html = llms_form_field( $opts, false ); - - $this->assertStringContains( '<div class="llms-form-field type-checkbox is-group', $html ); - $this->assertStringContains( '<label for="checkbox-id">Checkbox Label</label><div class="llms-field-checkbox llms-input-group"', $html ); - $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 ); - $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 ); - - // Default value. - $opts['default'] = 'opt1'; - $html = llms_form_field( $opts, false ); - $this->assertStringContains( - '<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 - ); - $this->assertStringNotContains( - '<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 - ); - - // Test multiple defaults. - $opts['default'] = array( 'opt1', 'opt2' ); - $html = llms_form_field( $opts, false ); - $this->assertStringContains( - '<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 - ); - $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 - ); - - // User has saved data. - $this->get_user_with_meta( 'checkbox-id', 'opt2' ); - $html = llms_form_field( $opts, false ); - $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 ); - $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 ); - - } - - /** - * Test button field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_button() { - - $html = llms_form_field( array( - 'type' => 'button', - 'value' => 'Button Text', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-button', $html ); - $this->assertStringContains( '<button class="llms-field-button"', $html ); - $this->assertStringContains( 'type="button"', $html ); - $this->assertStringContains( '>Button Text</button>', $html ); - - } - - /** - * Test submit button field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_submit() { - - $html = llms_form_field( array( - 'type' => 'submit', - 'value' => 'Button Text', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-submit', $html ); - $this->assertStringContains( '<button class="llms-field-button"', $html ); - $this->assertStringContains( 'type="submit"', $html ); - $this->assertStringContains( '>Button Text</button>', $html ); - - } - - /** - * Test reset button field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_reset() { - - $html = llms_form_field( array( - 'type' => 'reset', - 'value' => 'Button Text', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-reset', $html ); - $this->assertStringContains( '<button class="llms-field-button"', $html ); - $this->assertStringContains( 'type="reset"', $html ); - $this->assertStringContains( '>Button Text</button>', $html ); - } - - /** - * Test output of a text input field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_text() { - - $html = llms_form_field( array(), false ); - - $this->assertStringContains( '<div class="llms-form-field type-text', $html ); - $this->assertStringContains( '<input ', $html ); - $this->assertStringContains( 'type="text"', $html ); - - } - - /** - * Test email field type. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_email() { - - $html = llms_form_field( array( - 'type' => 'email', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-email', $html ); - $this->assertStringContains( '<input ', $html ); - $this->assertStringContains( 'type="email"', $html ); - - } - - /** - * Test tel field type. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_tel() { - - $html = llms_form_field( array( - 'type' => 'tel', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-tel', $html ); - $this->assertStringContains( '<input ', $html ); - $this->assertStringContains( 'type="tel"', $html ); - - } - - /** - * Test number field type. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_number() { - - $html = llms_form_field( array( - 'type' => 'number', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-number', $html ); - $this->assertStringContains( '<input ', $html ); - $this->assertStringContains( 'type="number"', $html ); - - } - - /** - * Test textarea field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_textarea() { - - $html = llms_form_field( array( - 'type' => 'textarea', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-textarea', $html ); - $this->assertStringContains( '<textarea class="llms-field-textarea"', $html ); - $this->assertStringContains( '></textarea>', $html ); - - } - - /** - * Test textarea field with user data. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_textarea_with_user_data() { - - $this->get_user_with_meta( 'textarea-id', 'Lorem ipsum dolor sit.' ); - - $html = llms_form_field( array( - 'id' => 'textarea-id', - 'type' => 'textarea', - ), false ); - - $this->assertStringContains( '>Lorem ipsum dolor sit.</textarea>', $html ); - - } - - /** - * Test custom html field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_type_html() { - - $html = llms_form_field( array( - 'type' => 'html', - 'value' => '<h2>HTML Content.</h2>', - ), false ); - - $this->assertStringContains( '<div class="llms-form-field type-html', $html ); - $this->assertStringContains( '<div class="llms-field-html"', $html ); - $this->assertStringContains( '><h2>HTML Content.</h2></div>', $html ); - - } - - /** - * Test attributes setting. - * - * @since 5.0.0 - * - * @return void - */ - public function test_attributes() { - - $this->assertStringContains( 'data-custom="whatever', llms_form_field( array( 'attributes' => array( 'data-custom' => 'whatever' ) ), false ) ); - - $multi = llms_form_field( array( 'attributes' => array( 'data-custom' => 'whatever', 'maxlength' => 5 ) ), false ); - $this->assertStringContains( 'maxlength="5"', $multi ); - $this->assertStringContains( 'data-custom="whatever', $multi ); - - } - - /** - * Test columns setting. - * - * @since 5.0.0 - * - * @return void - */ - public function test_columns() { - - // Default. - $this->assertStringContains( 'llms-cols-12 llms-cols-last', llms_form_field( array(), false ) ); - $this->assertStringContains( '<div class="clear"></div>', llms_form_field( array(), false ) ); - - // Set cols. - $this->assertStringContains( 'llms-cols-5 llms-cols-last', llms_form_field( array( 'columns' => 5 ), false ) ); - $this->assertStringContains( 'llms-cols-8 llms-cols-last', llms_form_field( array( 'columns' => 8 ), false ) ); - - // Not last. - $this->assertStringNotContains( 'llms-cols-last', llms_form_field( array( 'last_column' => false ), false ) ); - $this->assertStringNotContains( '<div class="clear"></div>', llms_form_field( array( 'last_column' => false ), false ) ); - - } - - /** - * Test id setting. - * - * @since 5.0.0 - * - * @return void - */ - public function test_id() { - - $this->assertStringContains( 'id="', llms_form_field( array(), false ) ); - $this->assertStringContains( 'id="mock"', llms_form_field( array( 'id' => 'mock' ), false ) ); - - } - - /** - * Test wrapper classes setting. - * - * @since 5.0.0 - * - * @return void - */ - public function test_wrapper_classes() { - - // Strings. - $this->assertStringContains( 'mock-wrapper-class">', llms_form_field( array( 'wrapper_classes' => 'mock-wrapper-class' ), false ) ); - $this->assertStringContains( 'mock-wrapper-class alt-class">', llms_form_field( array( 'wrapper_classes' => 'mock-wrapper-class alt-class' ), false ) ); - - // Arrays. - $this->assertStringContains( 'mock-wrapper-class">', llms_form_field( array( 'wrapper_classes' => array( 'mock-wrapper-class' ) ), false ) ); - $this->assertStringContains( 'mock-wrapper-class alt-class">', llms_form_field( array( 'wrapper_classes' => array( 'mock-wrapper-class', 'alt-class' ) ), false ) ); - - } - - /** - * Test field `value` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_value() { - - // No specified value. - $this->assertStringNotContains( 'value="', llms_form_field( array(), false ) ); - - // Value is specified. - $this->assertStringContains( 'value="mock"', llms_form_field( array( 'value' => 'mock' ), false ) ); - - // Default value specified. - $this->assertStringContains( 'value="mock"', llms_form_field( array( 'default' => 'mock' ), false ) ); - - // Default value not added if a value is specified. - $this->assertStringContains( 'value="mock"', llms_form_field( array( 'value' => 'mock', 'default' => 'fake' ), false ) ); - $this->assertStringNotContains( 'value="fake"', llms_form_field( array( 'value' => 'mock', 'default' => 'fake' ), false ) ); - - } - - /** - * Test field `name` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_name() { - - // No name specified, fallback to the field id. - $this->assertStringContains( 'name="mock"', llms_form_field( array( 'id' => 'mock' ), false ) ); - - // Name specified. - $this->assertStringContains( 'name="mock"', llms_form_field( array( 'name' => 'mock', 'id' => 'fake' ), false ) ); - - // Name explicitly disabled. - $this->assertStringNotContains( 'name="', llms_form_field( array( 'name' => false ), false ) ); - - } - - /** - * Test field `placeholder` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_placeholder() { - - $this->assertStringContains( 'placeholder="test"', llms_form_field( array( 'placeholder' => 'test' ), false ) ); - - } - - /** - * Test field `style` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_deprecated_attributes() { - - // No style. - $this->assertStringNotContains( 'style="', llms_form_field( array(), false ) ); - - // Has style. - $this->assertStringContains( 'style="test"', llms_form_field( array( 'style' => 'test' ), false ) ); - - $this->assertStringContains( 'maxlength="1"', llms_form_field( array( 'max_length' => '1' ), false ) ); - $this->assertStringContains( 'minlength="25"', llms_form_field( array( 'min_length' => '25' ), false ) ); - - } - - /** - * Test field description. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_description() { - - // No description. - $this->assertStringNotContains( '<span class="llms-description">', llms_form_field( array(), false ) ); - - // Has Description. - $this->assertStringContains( '<span class="llms-description">Test Description</span>', llms_form_field( array( 'description' => 'Test Description' ), false ) ); - - } - - /** - * Test field `required` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_required() { - - // Not required. - $this->assertStringNotContains( '<span class="llms-required">*</span>', llms_form_field( array( 'label' => 'mock' ), false ) ); - $this->assertStringNotContains( 'required="required"', llms_form_field( array(), false ) ); - - // Is required. - $this->assertStringContains( '<span class="llms-required">*</span>', llms_form_field( array( 'required' => true, 'label' => 'mock' ), false ) ); - $this->assertStringContains( 'required="required"', llms_form_field( array( 'required' => true ), false ) ); - - // Required but no label. - $this->assertStringNotContains( '<span class="llms-required">*</span>', llms_form_field( array( 'required' => true ), false ) ); - - } - - /** - * Test field `label` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_label() { - - $this->assertStringContains( '<label for="fake">mock</label>', llms_form_field( array( 'id' => 'fake', 'label' => 'mock' ), false ) ); - $this->assertStringContains( '<label for="fake">mock<span class="llms-required">*</span></label>', llms_form_field( array( 'id' => 'fake', 'label' => 'mock', 'required' => true ), false ) ); - - } - - /** - * No label element output when label is empty. - * - * @since 5.0.0 - * - * @return void - */ - public function test_label_empty() { - - $this->assertStringNotContains( '<label', llms_form_field( array( 'id' => 'fake' ), false ) ); - - } - - /** - * Output an empty label element if `label_show_empty` is true and `label` is empty. - * - * @since 5.0.0 - * - * @return void - */ - public function test_label_show_empty() { - - $this->assertStringContains( '<label for="fake"></label>', llms_form_field( array( 'id' => 'fake', 'label_show_empty' => true ), false ) ); - - } - - /** - * Test field `disabled` attribute. - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_disabled() { - - // No disabled. - $this->assertStringNotContains( 'disabled="disabled"', llms_form_field( array(), false ) ); - - // Has disabled. - $this->assertStringContains( 'disabled="disabled"', llms_form_field( array( 'disabled' => true ), false ) ); - - } - - public function test_prepare_value_for_button_and_html() { - - $types = array( - // Always have an explicit value. - 'button', 'reset', 'submit', 'html', - // May or may not have an explicit value. - 'text', - ); - - foreach ( $types as $type ) { - - $args = array( - 'type' => $type, - 'name' => 'test_field', - 'value' => 'A Value', - ); - - $field = new LLMS_Form_Field( $args ); - - $settings = $field->get_settings(); - - $this->assertEquals( $args['value'], $settings['value'] ); - - } - - } - - /** - * Test prepare_password_strength_meter() with default values. - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_password_strength_meter_default_values() { - - $field = new LLMS_Form_Field(); - - $handler = function( $args ) { - $this->assertEquals( array(), $args['blocklist'] ); - $this->assertEquals( 6, $args['min_length'] ); - $this->assertEquals( 'strong', $args['min_strength'] ); - return $args; - }; - add_filter( 'llms_password_strength_meter_settings', $handler ); - - LLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() ); - - remove_filter( 'llms_password_strength_meter_settings', $handler ); - } - - /** - * Test prepare_password_strength_meter with custom values - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_password_strength_meter_custom_values() { - - $field = new LLMS_Form_Field( array( - 'min_strength' => 'weak', - 'min_length' => 10, - ) ); - - $handler = function( $args ) { - $this->assertEquals( 10, $args['min_length'] ); - $this->assertEquals( 'weak', $args['min_strength'] ); - return $args; - }; - add_filter( 'llms_password_strength_meter_settings', $handler ); - - LLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() ); - $this->assertFalse( isset( $field->get_settings()['min_length'] ) ); - - remove_filter( 'llms_password_strength_meter_settings', $handler ); - } - - /** - * Test prepare_password_strength_meter() to ensure the minimum accepted value is 6 - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_password_strength_meter_min_length() { - - $field = new LLMS_Form_Field( array( - 'min_length' => 2, - ) ); - - $handler = function( $args ) { - $this->assertEquals( 6, $args['min_length'] ); - return $args; - }; - add_filter( 'llms_password_strength_meter_settings', $handler ); - - LLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() ); - $this->assertFalse( isset( $field->get_settings()['min_length'] ) ); - - remove_filter( 'llms_password_strength_meter_settings', $handler ); - } - - /** - * Test prepare_password_strength_meter() for script enqueue - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_password_strength_meter_assets() { - - $field = new LLMS_Form_Field(); - - // Not enqueued. - LLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() ); - $this->assertAssetNotEnqueued( 'script', 'password-strength-meter' ); - $this->assertFalse( llms()->assets->is_inline_enqueued( 'llms-pw-strength-settings' ) ); - - // Enqueued. - do_action( 'wp_enqueue_scripts' ); - LLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() ); - - $this->assertAssetIsEnqueued( 'script', 'password-strength-meter' ); - $this->assertTrue( llms()->assets->is_inline_enqueued( 'llms-pw-strength-settings' ) ); - - } - - /** - * Test prepare_value() for a password field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_value_for_password() { - - $field = new LLMS_Form_Field( array( - 'type' => 'password', - 'name' => 'test_field', - ) ); - - $settings = $field->get_settings(); - - $this->assertEmpty( $settings['value'] ); - - } - - /** - * Test prepare_value() with user-posted data - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_value_with_posted_data() { - - $this->mockPostRequest( array( - 'test_field' => 'submitted value', - ) ); - - $field = new LLMS_Form_Field( array( - 'name' => 'test_field', - ) ); - - $settings = $field->get_settings(); - - $this->assertEquals( 'submitted value', $settings['value'] ); - - } - - /** - * Test field html generated on submision when value is an array - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_array_value_only_post_request() { - - $checkbox_options = array( - 'yes_key' => 'Yes', - 'no_key' => 'No', - ); - - $form_field_conf = array( - 'name' => 'array_type', - 'type' => 'checkbox', - 'options' => $checkbox_options, - ); - - // Simulate a field submission where both the checkboxes are checked. - $this->mockPostRequest( - array( - 'array_type' => array_keys( $checkbox_options ), - ) - ); - - // Create a form field. - $form_field = llms_form_field( - $form_field_conf, - false - ); - - // Expect the html has 2 "checked" checkboxes. - $this->assertEquals( - 2, - substr_count( - $form_field, - '"checked"' - ) - ); - - // Simulate a field submission where only one checkbox is checked. - $this->mockPostRequest( - array( - 'array_type' => array_keys( $checkbox_options )[1], - ) - ); - - // Create a form field. - $form_field = llms_form_field( - $form_field_conf, - false - ); - - // Expect the html has 1 "checked" checkbox. - $this->assertEquals( - 1, - substr_count( - $form_field, - '"checked"' - ) - ); - - } - - /** - * Test llms_form_field when passing an user - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_with_user_as_data_source() { - - $opts = array( - 'id' => 'checkbox_store', - 'type' => 'checkbox', - 'options' => array( - 'mock_val' => 'Mock val', - 'mock_val2' => 'Mock val 2', - ), - 'data_store' => 'usermeta', - ); - - // User doesn't have value stored. - $this->get_user_with_meta( 'checkbox_store' ); - $user_id = get_current_user_id(); - // Log-out. - wp_set_current_user( null ); - - $html = llms_form_field( $opts, false, $user_id ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - // User has value stored. - $this->get_user_with_meta( 'checkbox_store', array( 'mock_val2' ) ); - $user_id = get_current_user_id(); - // Log-out. - wp_set_current_user( null ); - - $html = llms_form_field( $opts, false, $user_id ); - $this->assertStringContains( - '<input checked="checked" class="llms-field-checkbox" id="checkbox_store--mock_val2" name="checkbox_store[]" type="checkbox" value="mock_val2" />', - $html - ); - - // Log in the last user. - wp_set_current_user( $user_id ); - $html = llms_form_field( $opts, false, $user_id ); - $this->assertStringContains( - '<input checked="checked" class="llms-field-checkbox" id="checkbox_store--mock_val2" name="checkbox_store[]" type="checkbox" value="mock_val2" />', - $html - ); - - // Log in the last user. - wp_set_current_user( $user_id ); - // Pass a non existing user. - $html = llms_form_field( $opts, false, $user_id + 1 ); - $this->assertStringNotContains( 'checked="checked"', $html ); - - } - - /** - * Test LLMS_Form_Field data_source and data_source_type props setting - * - * @since 5.0.0 - * - * @return void - */ - public function test_field_data_source_and_type() { - $opts = array( - 'type' => 'button', - 'value' => 'Button Text', - 'data_store' => 'usermeta', - ); - - // Pass a WP Post in place of a user. - $post = $this->factory->post->create_and_get(); - $field = new LLMS_Form_Field( $opts, $post ); - - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) ); - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) ); - - // Pass a WP User. - $user = $this->factory->user->create_and_get(); - $field = new LLMS_Form_Field( $opts, $user ); - - $this->assertEquals( $user, LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) ); - $this->assertEquals( 'wp_user', LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) ); - - // Pass a WP User ID. - $opts['data_store'] = 'users'; // Test it works with the users table as store too. - $field = new LLMS_Form_Field( $opts, $user->ID ); - - $this->assertEquals( $user, LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) ); - $this->assertEquals( 'wp_user', LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) ); - - // Pass a non existing WP User ID. - $field = new LLMS_Form_Field( $opts, $user->ID + 1 ); - - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) ); - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) ); - - // Pass an existing WP User ID but change the data_store to something different from 'usermeta' or 'users'. - $opts['data_store'] = 'whatever'; - $field = new LLMS_Form_Field( $opts, $user->ID ); - - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) ); - $this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-form-handler.php b/tests/phpunit/unit-tests/forms/class-llms-test-form-handler.php deleted file mode 100644 index f178ad5b9a..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-form-handler.php +++ /dev/null @@ -1,540 +0,0 @@ -<?php -/** - * Test Form Handler class - * - * @package LifterLMS/Tests - * - * @group forms - * @group form_handler - * - * @since 5.0.0 - * @version 5.4.1 - */ -class LLMS_Test_Form_Handler extends LLMS_UnitTestCase { - - /** - * Setup the test case. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->handler = LLMS_Form_Handler::instance(); - - // Actions aren't firing on unit tests without explicitly calling the constructor to add them. Not sure why. - LLMS_Unit_Test_Util::call_method( $this->handler, '__construct' ); - - LLMS_Forms::instance()->install( true ); - - } - - /** - * Teardown the test. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - global $wpdb; - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - } - - public function make_address_required( $settings ) { - - if ( 0 === strpos( $settings['name'], 'llms_billing_' ) && 'llms_billing_address_2' !== $settings['name'] ) { - $settings['required'] = true; - } - - return $settings; - } - - protected function get_data_for_form_submit( $args = array() ) { - - $email = uniqid( 'fake-' ) . '@mock.tld'; - - return wp_parse_args( $args, array( - 'email_address' => $email, - 'email_address_confirm' => $email, - 'password' => '12345678', - 'password_confirm' => '12345678', - 'first_name' => 'Jeffrey', - 'last_name' => 'Lebowski', - 'llms_billing_address_1' => '123 Any Street', - 'llms_billing_city' => 'Reseda', - 'llms_billing_state' => 'CA', - 'llms_billing_zip' => '91234', - 'llms_billing_country' => 'US', - ) ); - - } - - /** - * Test submit() for the account form when there's no logged in user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_account_no_user() { - - $ret = $this->handler->submit( array(), 'account' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-no-user', $ret ); - - } - - /** - * Test submit() for the account for when there is a logged in user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_account_with_user() { - - wp_set_current_user( $this->factory->student->create() ); - $ret = $this->handler->submit( array(), 'account' ); - - // We're still going to get an error but it won't be the "llms-form-no-user" error. - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret ); - - } - - /** - * Test submit on an invalid form location. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_invalid() { - - $ret = $this->handler->submit( array(), 'fake' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-invalid-location', $ret ); - - } - - /** - * Test submit with missing required fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_missing_required() { - - $ret = $this->handler->submit( array(), 'checkout' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret ); - - } - - /** - * Test custom fields added the legacy way are correctly parsed - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_custom_field_legacy() { - - $custom_fields = array( - array( - 'columns' => 12, - 'id' => 'llms_company_name', - 'label' => 'Company name', - 'last_column' => false, - 'required' => true, - 'type' => 'text', - ), - ); - - add_filter( - 'lifterlms_get_person_fields', - function( $fields, $screen ) use ( $custom_fields ) { - array_push( $fields, ...$custom_fields ); - return $fields; - }, - 10, - 2 - ); - - $args = $this->get_data_for_form_submit(); - - $ret = $this->handler->submit( $args, 'checkout' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret ); - - $args[ 'llms_company_name' ] = 'something'; - - $ret = $this->handler->submit( $args, 'checkout' ); - - $this->assertTrue( is_int( $ret ) ); - $this->assertEquals( 'something', get_user_meta( $ret, 'llms_company_name', true ) ); - - remove_all_filters( 'lifterlms_get_person_fields' ); - - } - - /** - * Test submission matching errors. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_matching_errors() { - - $args = array( - 'email_address' => 'fake@mock.com', - 'email_address_confirm' => 'mismatch@mock.com', - 'password' => '12345678', - 'password_confirm' => 'mistmatch', - 'first_name' => 'Jeffrey', - 'last_name' => 'Lebowski', - 'llms_billing_address_1' => '123 Any Street', - 'llms_billing_city' => 'Reseda', - 'llms_billing_state' => 'CA', - 'llms_billing_zip' => '91234', - 'llms_billing_country' => 'US', - ); - - $ret = $this->handler->submit( $args, 'checkout' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-not-matched', $ret ); - - } - - /** - * Test registration form submissions with an invalid voucher code. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_registration_voucher_err_not_found() { - - $ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => 'invalid-code' ) ), 'registration' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( 'Voucher code "invalid-code" could not be found.', $ret ); - - } - - /** - * Test registration form submissions with a deleted voucher code. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_registration_voucher_err_deleted() { - - $voucher = $this->create_voucher( 1, 1 ); - $code = $voucher->get_voucher_codes()[0]; - $voucher->delete_voucher_code( $code->id ); - - $ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ), 'registration' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( sprintf( 'Voucher code "%s" could not be found.', $code->code ), $ret ); - - } - - /** - * Test registration form submissions when a voucher code's parent post is deleted (or not published). - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_registration_voucher_err_post_deleted() { - - $voucher = $this->create_voucher( 1, 1 ); - $code = $voucher->get_voucher_codes()[0]; - wp_delete_post( $code->voucher_id, true ); - - $ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ) , 'registration' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( sprintf( 'Voucher code "%s" is no longer valid.', $code->code ), $ret ); - - } - - /** - * Test registration form submissions when a voucher code has been redeemed the maximum number of times allowed - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_registration_voucher_err_max() { - - $voucher = $this->create_voucher( 1, 1 ); - $code = $voucher->get_voucher_codes()[0]; - $voucher->use_voucher( $code->code, $this->factory->user->create() ); - - $ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ), 'registration' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( sprintf( 'Voucher code "%s" has already been redeemed the maximum number of times.', $code->code ), $ret ); - - } - - /** - * Test successful submission for a new users. - * - * @since 5.0.0 - * @since 5.1.0 Provide `password_current` when updating the `password`. - * - * @return void - */ - public function test_submit_success() { - - $args = $this->get_data_for_form_submit(); - - // Register. - $ret = $this->handler->submit( $args, 'checkout' ); - - $this->assertTrue( is_int( $ret ) ); - $user = new WP_User( $ret ); - - $this->assertEquals( $args['email_address'], $user->user_email ); - $this->assertEquals( $args['first_name'], $user->first_name ); - $this->assertEquals( $args['last_name'], $user->last_name ); - - $this->assertEquals( $args['llms_billing_address_1'], $user->llms_billing_address_1 ); - $this->assertEquals( $args['llms_billing_city'], $user->llms_billing_city ); - $this->assertEquals( $args['llms_billing_state'], $user->llms_billing_state ); - $this->assertEquals( $args['llms_billing_zip'], $user->llms_billing_zip ); - $this->assertEquals( $args['llms_billing_country'], $user->llms_billing_country ); - - $this->assertTrue( wp_check_password( $args['password'], $user->user_pass, $user->ID ) ); - - // Update. - wp_set_current_user( $ret ); - $args['first_name'] = 'Maude'; - $args['display_name'] = $user->display_name; - // Current password is required when updating the password. - $args['password_current'] = $args['password']; - $this->assertSame( $ret, $this->handler->submit( $args, 'account' ) ); - $this->assertEquals( $args['first_name'], $user->first_name ); - - } - - - /** - * Test submitting account update without password update - * - * @since 5.1.0 - * - * @return void - */ - public function test_submit_password_update_wrong_current_password() { - - $args = $this->get_data_for_form_submit( - array( - 'display_name' => 'Disp', // Required on update. - ) - ); - - // Register. - $ret = $this->handler->submit( $args, 'checkout' ); - - $this->assertTrue( is_int( $ret ) ); - $user = new WP_User( $ret ); - - // Update. - wp_set_current_user( $ret ); - $args['first_name'] = 'Maude'; - unset($args['password']); - unset($args['password_confirm']); - - $this->assertSame( $ret, $this->handler->submit( $args, 'account' ) ); - $this->assertEquals( $args['first_name'], $user->first_name ); - - } - - /** - * Test submit password change without providing, or with wrong current password - * - * @since 5.1.0 - * - * @return void - */ - public function test_submit_account_update_no_password() { - - $args = $this->get_data_for_form_submit( - array( - 'display_name' => 'Disp', // Required on update. - ) - ); - - // Register. - $ret = $this->handler->submit( $args, 'checkout' ); - - $this->assertTrue( is_int( $ret ) ); - $user = new WP_User( $ret ); - - // Update. - wp_set_current_user( $ret ); - - // No current password provided. - $ret = $this->handler->submit( $args, 'account' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret ); - $this->assertWPErrorMessageEquals( 'Current Password is a required field.', $ret ); - - // Provide a wrong current password. - $args['password_current'] = $args['password'] . "-wrong"; - $ret = $this->handler->submit( $args, 'account' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( 'The submitted password was not correct.', $ret ); - - } - - /** - * Test submit() with a country that doesn't require states. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_address_no_zip() { - - add_filter( 'llms_field_settings', array( $this, 'make_address_required' ) ); - - $args = $this->get_data_for_form_submit( array( - 'llms_billing_state' => 'C', - 'llms_billing_country' => 'UG', // Uganda. - ) ); - unset( $args['llms_billing_zip'] ); - - $ret = $this->handler->submit( $args, 'checkout' ); - $this->assertTrue( is_numeric( $ret ) ); - - remove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 ); - - } - - /** - * Test submit() with a country that doesn't require zip codes. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_address_no_states() { - - add_filter( 'llms_field_settings', array( $this, 'make_address_required' ) ); - - $args = $this->get_data_for_form_submit( array( - 'llms_billing_zip' => '23424', - 'llms_billing_country' => 'AS', // America Samoa. - ) ); - unset( $args['llms_billing_state'] ); - - $ret = $this->handler->submit( $args, 'checkout' ); - $this->assertTrue( is_numeric( $ret ) ); - - remove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 ); - - } - - /** - * Test submit() with a country that doesn't require states or zip codes. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_address_no_city_or_zip() { - - add_filter( 'llms_field_settings', array( $this, 'make_address_required' ) ); - - $args = $this->get_data_for_form_submit( array( - 'llms_billing_country' => 'NR', - 'llms_billing_state' => '08', - ) ); - unset( $args['llms_billing_city'] ); - unset( $args['llms_billing_zip'] ); - - $ret = $this->handler->submit( $args, 'checkout' ); - $this->assertTrue( is_numeric( $ret ) ); - - remove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 ); - - } - - /** - * Test successful submission for a new users. - * - * @since 5.0.0 - * - * @return void - */ - public function test_submit_success_with_voucher() { - - $voucher = $this->get_mock_voucher( 1 ); - $products = $voucher->get_products(); - $code = $voucher->get_voucher_codes()[0]->code; - - $args = $this->get_data_for_form_submit( array( 'llms_voucher' => $code ) ); - - $ret = $this->handler->submit( $args, 'registration' ); - - $this->assertTrue( is_int( $ret ) ); - $user = new WP_User( $ret ); - - // Ensure voucher was redeemed successfully. - foreach ( $products as $product_id ) { - llms_is_user_enrolled( $user->ID, $product_id, 'all', false ); - } - - } - - /** - * Tests that submitting a invalid email address produces an error. - * - * @since 5.4.1 - * - * @return void - */ - public function test_submit_registration_with_invalid_email_error() { - - // Disable user generation. - add_filter( 'pre_option_lifterlms_registration_generate_username', '__return_empty_string' ); - LLMS_Forms::instance()->create( 'registration', true ); - - $args = $this->get_data_for_form_submit( - array( - 'user_login' => 'the_dude', - 'email_address' => 'fake@wrong', - 'email_address_confirm' => 'fake@wrong', - ) - ); - - - $ret = $this->handler->submit( $args, 'registration' ); - $this->assertIsWPError( $ret ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret ); - $this->assertWPErrorMessageEquals( 'The email address "fake@wrong" is not valid.', $ret ); - - // Re-enable user generation. - remove_filter( 'pre_option_lifterlms_registration_generate_username', '__return_empty_string' ); - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-form-post-type.php b/tests/phpunit/unit-tests/forms/class-llms-test-form-post-type.php deleted file mode 100644 index 42d9cd608a..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-form-post-type.php +++ /dev/null @@ -1,311 +0,0 @@ -<?php -/** - * Test LLMS_Form_Post_Type class - * - * @package LifterLMS/Tests - * - * @group forms - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Form_Post_Type extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Form_Post_Type( LLMS_Forms::instance() ); - - } - - /** - * Test class properties. - * - * @since 5.0.0 - * - * @return void - */ - public function test_properties() { - - $this->assertEquals( 'llms_form', $this->main->post_type ); - $this->assertEquals( 'manage_lifterlms', $this->main->capability ); - - } - - /** - * Test enabled_post_type_visibility() when skipping the override - * - * @since 5.0.0 - * - * @return void - */ - public function test_enable_post_type_visibility() { - - $res_data = array( 'viewable' => false ); - $res = rest_ensure_response( $res_data ); - $post_type = get_post_type_object( 'llms_form' ); - - // Not admin. - $this->assertEquals( $res_data, $this->main->enable_post_type_visibility( $res, $post_type )->get_data() ); - - // Is admin. - set_current_screen( 'admin.php' ); - - // Wrong post type. - $this->assertEquals( $res_data, $this->main->enable_post_type_visibility( $res, get_post_type_object( 'course' ) )->get_data() ); - - // Okay. - $this->assertEquals( array( 'viewable' => true ), $this->main->enable_post_type_visibility( $res, $post_type )->get_data() ); - - set_current_screen( 'front' ); // Reset. - - } - - /** - * Test permalink retrieval for account updates. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_permalink_for_account() { - - LLMS_Install::create_pages(); - - $url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'account' ) ) ); - parse_str( $url['query'], $qs ); - - $this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] ); - $this->assertEquals( get_option( 'lifterlms_myaccount_page_id' ), $qs['page_id'] ); - $this->assertArrayHasKey( 'edit-account', $qs ); - - } - - /** - * Test permalink retrieval for checkout when no access plans exist. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_permalink_for_checkout_no_plans() { - - global $wpdb; - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_access_plan' ) ); - - LLMS_Install::create_pages(); - - $url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'checkout' ) ) ); - parse_str( $url['query'], $qs ); - - $this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] ); - $this->assertEquals( get_option( 'lifterlms_checkout_page_id' ), $qs['page_id'] ); - $this->assertEquals( 'visitor', $qs['llms-view-as'] ); - - $this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) ); - - } - - /** - * Test permalink retrieval for checkout with access plans. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_permalink_for_checkout_with_plans() { - - LLMS_Install::create_pages(); - $plan = $this->get_mock_plan(); - - $url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'checkout' ) ) ); - parse_str( $url['query'], $qs ); - - $this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] ); - $this->assertEquals( get_option( 'lifterlms_checkout_page_id' ), $qs['page_id'] ); - $this->assertEquals( 'visitor', $qs['llms-view-as'] ); - $this->assertEquals( $plan->get( 'id' ), $qs['plan'] ); - - $this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) ); - - } - - /** - * Test permalink retrieval for registration form when open registration is not enabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_permalink_for_registration_not_enabled() { - - $form = get_post( LLMS_Forms::instance()->create( 'registration' ) ); - update_option( 'lifterlms_enable_myaccount_registration', 'no' ); - $this->assertFalse( get_permalink( LLMS_Forms::instance()->create( 'registration' ) ) ); - - } - - /** - * Test permalink retrieval for registration form when open registration is enabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_permalink_for_registration_enabled() { - - LLMS_Install::create_pages(); - update_option( 'lifterlms_enable_myaccount_registration', 'yes' ); - - $url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'registration' ) ) ); - parse_str( $url['query'], $qs ); - - $this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] ); - $this->assertEquals( get_option( 'lifterlms_myaccount_page_id' ), $qs['page_id'] ); - $this->assertEquals( 'visitor', $qs['llms-view-as'] ); - - $this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) ); - - } - - /** - * Test maybe_prevent_deletion() for other post types - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_prevent_deletion_wrong_post_type() { - $post = $this->factory->post->create_and_get(); - $this->assertNull( $this->main->maybe_prevent_deletion( null, $post ) ); - } - - /** - * Test maybe_prevent_deletion() for non-core forms - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_prevent_deletion_not_core() { - $post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_form' ) ); - $this->assertNull( $this->main->maybe_prevent_deletion( null, $post ) ); - } - - /** - * Test maybe_prevent_deletion() for core forms that cannot be deleted. - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_prevent_deletion() { - $post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_form' ) ); - update_post_meta( $post->ID, '_llms_form_is_core', 'yes' ); - $this->assertFalse( $this->main->maybe_prevent_deletion( null, $post ) ); - } - - /** - * Test meta_auth_callback() - * - * @since 5.0.0 - * - * @return void - */ - public function test_meta_auth_callback() { - - LLMS_Install::create_pages(); - $form = get_post( LLMS_Forms::instance()->create( 'registration' ) ); - - $roles = array( - 'administrator' => true, - 'lms_manager' => true, - 'instructor' => false, - 'student' => false, - 'editor' => false, - 'subscriber' => false, - ); - - // Logged out user can't do stuff. - $this->assertFalse( $this->main->meta_auth_callback( false, 'does_not_matter', $form->ID, null, 'does_not_matter', array() ) ); - - // Test various roes. - foreach ( $roles as $role => $expect ) { - $user = $this->factory->user->create_and_get( array( 'role' => $role ) ); - $this->assertSame( $expect, $this->main->meta_auth_callback( false, 'does_not_matter', $form->ID, $user->ID, 'does_not_matter', $user->caps ) ); - } - - - } - - /** - * Test post type registration. - * - * @since 5.0.0 - * - * @return void - */ - public function test_register_post_type() { - - // Remove it so we can ensure we register it. - unregister_post_type( 'llms_form' ); - - // Make sure the filter runs. - global $form_post_type_registrion_runs; - $filter_ran = 0; - $handler = function( $name ) { - global $form_post_type_registrion_runs; - ++$form_post_type_registrion_runs; - return $name; - }; - add_filter( 'lifterlms_register_post_type_form', $handler ); - - $this->main->register_post_type(); - - // Post type has been registered. - $this->assertTrue( post_type_exists( 'llms_form' ) ); - - // Filter ran. - $this->assertEquals( 1, $form_post_type_registrion_runs ); - - remove_filter( 'lifterlms_register_post_type_form', $handler ); - - } - - /** - * Test custom meta prop registration. - * - * @since 5.0.0 - * - * @return void - */ - public function test_register_meta() { - - do_action( 'init' ); - - global $wp_meta_keys; - $this->assertArrayHasKey( 'post', $wp_meta_keys ); - $this->assertArrayHasKey( 'llms_form', $wp_meta_keys['post'] ); - - // Expected meta props. - $props = array( - '_llms_form_location', - '_llms_form_show_title', - '_llms_form_is_core', - ); - - foreach ( $props as $meta ) { - $this->assertArrayHasKey( $meta, $wp_meta_keys['post']['llms_form'] ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-form-templates.php b/tests/phpunit/unit-tests/forms/class-llms-test-form-templates.php deleted file mode 100644 index 441348563f..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-form-templates.php +++ /dev/null @@ -1,560 +0,0 @@ -<?php -/** - * Test LLMS_Form_Templates class - * - * @package LifterLMS/Tests - * - * @group form_templates - * - * @since 5.0.0 - */ -class LLMS_Test_Form_Templates extends LLMS_Unit_Test_Case { - - /** - * Ensures the generated block content of a reusable block matches the stored "snapshot" - * - * @since 5.0.0 - * @since 5.3.1 Remove duplicate references to last-name block. - * - * @param string $id Field ID. - * @param string $actual Actual generated content to compare to the expected "snapshot" - * @return void - */ - public function assertReusableBlockContentMatchesSnapshot( $id, $actual ) { - - $snapshots = array( - '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"} /-->', - '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 -->', - '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":"strong","html_attrs":{"minlength":8},"meter_description":"A strong 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":"strong","html_attrs":{"minlength":8},"meter_description":"A strong 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 -->', - '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 -->', - '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"} /-->', - '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 -->', - '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} /-->', - ); - - // Parse blocks for comparison, mostly because we don't care about the order of attributes. - $expected = parse_blocks( $snapshots[ $id ] ); - $actual = parse_blocks( $actual ); - - $this->assertEquals( $expected, $actual, $id ); - - } - - /** - * Retrieve a list of field ids as they are to be stored on a template at a given location - * - * @since 5.0.0 - * - * @param string $location A form location ID. - * @return string[] - */ - private function get_template_field_id_list( $location ) { - - $blocks = $this->get_template( $location ); - $list = array(); - - foreach ( $blocks as $block ) { - - if ( 'core/block' === $block['blockName'] ) { - $list[] = get_post_meta( $block['attrs']['ref'], '_llms_field_id', true ); - } elseif ( 'llms/form-field-redeem-voucher' === $block['blockName'] ) { - $list[] = 'voucher'; - } else { - $list[] = $block['blockName']; - } - - } - - return $list; - - } - - private function get_template( $location ) { - - $res = LLMS_Form_Templates::get_template( $location ); - return parse_blocks( $res ); - - } - - private function get_block_from_template( $location, $name ) { - $blocks = $this->get_template( $location ); - - foreach ( $blocks as $block ) { - if ( $name === $block['blockName'] ) { - return $block; - } - } - - return false; - - } - - /** - * Test create_reusable_block() - * - * @since 5.0.0 - * - * @return void - */ - public function test_create_and_get_reusable_block() { - - $list = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-reusable-blocks.php'; - - foreach ( $list as $field_id => $def ) { - - $post_id = LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'create_reusable_block', array( $field_id ) ); - $this->assertTrue( ! empty( $post_id ) && is_int( $post_id ) ); - - $post = get_post( $post_id ); - - // Title stored. - $this->assertEquals( $def['title'], $post->post_title ); - - // Block(s) inserted correctly. - $this->assertReusableBlockContentMatchesSnapshot( $field_id, $post->post_content ); - - // Meta data is stored. - $this->assertEquals( 'yes', get_post_meta( $post_id, '_is_llms_field', true ) ); - $this->assertEquals( $field_id, get_post_meta( $post_id, '_llms_field_id', true ) ); - - // If we try to create it again the existing post will be used (in favor of creating a new one). - $this->assertEquals( $post_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'create_reusable_block', array( $field_id ) ) ); - - // Retrieve the core/block array. - $expected = array( - 'blockName' => 'core/block', - 'attrs' => array( - 'ref' => $post_id, - ), - 'innerContent' => array(), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( $field_id ) ) ); - - } - - } - - /** - * Test get_template() for the account location during a clean installation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_account_clean() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - $expected = array( - 'name', - 'display_name', - 'address', - 'phone', - 'email', - 'password', - ); - - $this->assertEquals( $expected, $this->get_template_field_id_list( 'account' ) ); - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - } - - /** - * Test get_template() for the account location during an upgrade. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_account_update() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - $opts = array( - 'lifterlms_user_info_field_email_confirmation_account_visibility' => 'no', - 'lifterlms_user_info_field_address_account_visibility' => 'hidden', - 'lifterlms_user_info_field_names_account_visibility' => 'optional', - 'lifterlms_user_info_field_phone_account_visibility' => 'required', - ); - - foreach ( $opts as $key => $val ) { - update_option( $key, $val ); - } - - // Expected List. - $expected = array( - 'llms/form-field-user-name', - 'llms/form-field-user-display-name', - 'llms/form-field-user-phone', - 'llms/form-field-user-email', - 'llms/form-field-confirm-group', - ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'account' ) ); - - // Password has confirm group. - $pass = $this->get_block_from_template( 'account', 'llms/form-field-confirm-group' ); - $this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] ); - $this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] ); - - // No confirm field on email. - $email = $this->get_block_from_template( 'account', 'llms/form-field-user-email' ); - $this->assertEquals( array(), $email['innerBlocks'] ); - - // Names are optional. - $name = $this->get_block_from_template( 'account', 'llms/form-field-user-name' ); - $this->assertFalse( $name['innerBlocks'][0]['attrs']['required'] ); - $this->assertFalse( $name['innerBlocks'][1]['attrs']['required'] ); - - // Phone is required. - $phone = $this->get_block_from_template( 'account', 'llms/form-field-user-phone' ); - $this->assertTrue( $phone['attrs']['required'] ); - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - } - - /** - * Test get_template() for the checkout location during a clean installation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_checkout_clean() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - $expected = array( - 'email', - 'password', - 'name', - 'address', - 'phone', - ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) ); - - // With username. - update_option( 'lifterlms_registration_generate_username', 'no' ); - array_unshift( $expected, 'username' ); - - $this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) ); - - delete_option( 'lifterlms_registration_generate_username' ); - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - } - - /** - * Test get_template() for the checkout location during an upgrade. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_checkout_update() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - $opts = array( - 'lifterlms_user_info_field_email_confirmation_checkout_visibility' => 'no', - 'lifterlms_user_info_field_address_checkout_visibility' => 'hidden', - 'lifterlms_user_info_field_names_checkout_visibility' => 'optional', - 'lifterlms_user_info_field_phone_checkout_visibility' => 'required', - ); - - foreach ( $opts as $key => $val ) { - update_option( $key, $val ); - } - - // Expected List. - $expected = array( - 'llms/form-field-user-email', - 'llms/form-field-confirm-group', - 'llms/form-field-user-name', - 'llms/form-field-user-phone', - ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) ); - - // Password has confirm group. - $pass = $this->get_block_from_template( 'account', 'llms/form-field-confirm-group' ); - $this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] ); - $this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] ); - - // No confirm field on email. - $email = $this->get_block_from_template( 'checkout', 'llms/form-field-user-email' ); - $this->assertEquals( array(), $email['innerBlocks'] ); - - // Names are optional. - $name = $this->get_block_from_template( 'checkout', 'llms/form-field-user-name' ); - $this->assertFalse( $name['innerBlocks'][0]['attrs']['required'] ); - $this->assertFalse( $name['innerBlocks'][1]['attrs']['required'] ); - - // Phone is required. - $phone = $this->get_block_from_template( 'checkout', 'llms/form-field-user-phone' ); - $this->assertTrue( $phone['attrs']['required'] ); - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - } - - /** - * Test get_template() for the registration location during a clean installation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_registration_clean() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - $expected = array( - 'email', - 'password', - 'name', - 'address', - 'phone', - ); - - // No voucher. - update_option( 'lifterlms_voucher_field_registration_visibility', 'hidden' ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) ); - - // With username. - update_option( 'lifterlms_registration_generate_username', 'no' ); - array_unshift( $expected, 'username' ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) ); - - // With voucher. - delete_option( 'lifterlms_voucher_field_registration_visibility' ); - $expected[] = 'voucher'; - $this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) ); - - delete_option( 'lifterlms_registration_generate_username' ); - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' ); - - } - - /** - * Test get_template() for the registration location during an upgrade. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_template_registration_update() { - - add_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - $opts = array( - 'lifterlms_user_info_field_email_confirmation_registration_visibility' => 'yes', - 'lifterlms_user_info_field_address_registration_visibility' => 'required', - 'lifterlms_user_info_field_names_registration_visibility' => 'hidden', - 'lifterlms_user_info_field_phone_registration_visibility' => 'hidden', - ); - - foreach ( $opts as $key => $val ) { - update_option( $key, $val ); - } - - // Expected List. - $expected = array( - 'llms/form-field-confirm-group', // Email - 'llms/form-field-confirm-group', // Password - 'llms/form-field-user-address', - 'voucher', - ); - $this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) ); - - $blocks = $this->get_template( 'registration' ); - - // Confirm field on email. - $email = $blocks[0]; - $this->assertEquals( 'llms/form-field-user-email', $email['innerBlocks'][0]['blockName'] ); - $this->assertEquals( 'llms/form-field-text', $email['innerBlocks'][1]['blockName'] ); - - // Password has confirm group. - $pass = $blocks[1]; - $this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] ); - $this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] ); - - // Address is required are optional. - $address = $this->get_block_from_template( 'registration', 'llms/form-field-user-address' ); - $this->assertTrue( $address['innerBlocks'][0]['innerBlocks'][0]['attrs']['required'] ); // Line 1. - $this->assertFalse( $address['innerBlocks'][0]['innerBlocks'][1]['attrs']['required'] ); // Line 2. - $this->assertTrue( $address['innerBlocks'][1]['attrs']['required'] ); // City. - $this->assertTrue( $address['innerBlocks'][2]['attrs']['required'] ); // Country. - $this->assertTrue( $address['innerBlocks'][3]['innerBlocks'][0]['attrs']['required'] ); // State. - $this->assertTrue( $address['innerBlocks'][3]['innerBlocks'][1]['attrs']['required'] ); // Postal code. - - remove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' ); - - } - - - /** - * Test get_voucher_block() when the voucher field is disabled - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_voucher_block_disabled() { - - update_option( 'lifterlms_voucher_field_registration_visibility', 'hidden' ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) ); - - delete_option( 'lifterlms_voucher_field_registration_visibility' ); - - } - - /** - * Test get_voucher_block() when voucher submission is optional. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_voucher_block_optional() { - - update_option( 'lifterlms_voucher_field_registration_visibility', 'optional' ); - - $expected = array( - 'blockName' => 'llms/form-field-redeem-voucher', - 'attrs' => array( - 'id' => 'llms_voucher', - 'label' => __( 'Have a voucher?', 'lifterlms' ), - 'placeholder' => __( 'Voucher Code', 'lifterlms' ), - 'required' => false, - 'toggleable' => true, - 'data_store' => false, - 'data_store_key' => false, - ), - 'innerContent' => array(), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) ); - - delete_option( 'lifterlms_voucher_field_registration_visibility' ); - - } - - /** - * Test get_voucher_block() when voucher submission is required. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_voucher_block_required() { - - update_option( 'lifterlms_voucher_field_registration_visibility', 'required' ); - - $expected = array( - 'blockName' => 'llms/form-field-redeem-voucher', - 'attrs' => array( - 'id' => 'llms_voucher', - 'label' => __( 'Have a voucher?', 'lifterlms' ), - 'placeholder' => __( 'Voucher Code', 'lifterlms' ), - 'required' => true, - 'toggleable' => true, - 'data_store' => false, - 'data_store_key' => false, - ), - 'innerContent' => array(), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) ); - - delete_option( 'lifterlms_voucher_field_registration_visibility' ); - - } - - /** - * Test prepare_blocks(): Missing properties automatically added. - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_blocks_without_props() { - - $input = array( - array(), - ); - $expected = array( - array( - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerContent' => array(), - ), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) ); - - } - - /** - * Test prepare_blocks(): Existing props not overwritten. - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_blocks_with_props() { - - $input = array( - array( - 'attrs' => 'fake', - 'innerBlocks' => array(), - ), - ); - $expected = array( - array( - 'attrs' => 'fake', - 'innerBlocks' => array(), - 'innerContent' => array(), - ), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) ); - - } - - /** - * Test prepare_blocks(): Works recursively on inner blocks and fills innerContent. - * - * @since 5.0.0 - * - * @return void - */ - public function test_prepare_blocks_recursive() { - - $input = array( - array( - 'attrs' => array(), - 'innerBlocks' => array( array(), array() ), - ), - ); - $expected = array( - array( - 'attrs' => array(), - 'innerBlocks' => array_fill( 0, 2, array( - 'attrs' => array(), - 'innerBlocks' => array(), - 'innerContent' => array(), - ) ), - 'innerContent' => array( null, null ), - ), - ); - - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) ); - - } - - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-form-validator.php b/tests/phpunit/unit-tests/forms/class-llms-test-form-validator.php deleted file mode 100644 index 1ff4368dd2..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-form-validator.php +++ /dev/null @@ -1,984 +0,0 @@ -<?php -/** - * Test Form Handler class - * - * @package LifterLMS/Tests - * - * @group forms - * @group form_validator - * - * @since 5.0.0 - */ -class LLMS_Test_Form_Validator extends LLMS_UnitTestCase { - - /** - * Data for testing text and textarea field sanitization - * - * Each array is a numeric array with the first item being the "dirty" data and the second item - * being the expected result. - * - * @link https://github.com/WordPress/wordpress-develop/blob/master/tests/phpunit/tests/formatting/SanitizeTextField.php Most data adapted from WP Core tests. - * - * @since 5.0.0 - * - * @return array - */ - protected function data_for_text_fields() { - - return array( - array( - 'оРангутанг', - 'оРангутанг', - ), - array( - 'one is < two', - 'one is < two', - ), - array( - 'no <span>tags</span> <em>allowed</em> here <br>', - 'no tags allowed here', - ), - array( - ' trimmed ' , - 'trimmed', - ), - array( - 'No %AB octets %ab', - 'No octets', - ), - array( - 'emails@are.okay', - 'emails@are.okay', - ), - array( - array(), - array(), - ), - array( - llms(), - '', - ), - array( - false, - '', - ), - array( - true, - '1', - ), - array( - array( - 'text 1', - 'text 2', - false, - ), - array( - 'text 1', - 'text 2', - '', - ), - ), - ); - - } - - protected function get_field_arr( $type, $args = array() ) { - return wp_parse_args( $args, array( - 'id' => "field-{$type}-id", - 'name' => "field-{$type}-name", - 'type' => $type, - ) ); - } - - /** - * Setup the test case. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Form_Validator(); - - } - - /** - * Test reducing a fields array down to only the required fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_required_fields() { - - $required = array( - array( - 'name' => 'good', - 'required' => true, - ), - array( - 'name' => 'good2', - 'required' => true, - ), - ); - - $optional = array( - array( - 'name' => 'bad', - 'required' => false, - ), - ); - - // Only has required fields in the array. - $this->assertEquals( $required, $this->main->get_required_fields( $required ) ); - - // Only has optional fields. - $this->assertEquals( array(), $this->main->get_required_fields( $optional ) ); - - // Has both required and optional. - $this->assertEquals( $required, $this->main->get_required_fields( array_merge( $optional, $required ) ) ); - - } - - /** - * Test validate_fields() when no user input is supplied. - * - * @since 5.0.0 - * @since 5.1.0 Added test when the form field is not empty. - * - * @return void - */ - public function test_validate_fields_empty_input() { - - // Empty input - empty form => validates. - $res = $this->main->validate_fields( array(), array() ); - $this->assertTrue( $res ); - - // Empty input - not empty form => doesn't validate. - $res = $this->main->validate_fields( array(), array( $this->get_field_arr( 'text' ) ) ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms-form-no-input', $res ); - - } - - /** - * Test sanitize_field() for text fields / default case. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_default() { - - $tests = $this->data_for_text_fields(); - - $tests[] = array( - "no \nnewlines", - 'no newlines', - ); - $tests[] = array( - "no \ttabs", - 'no tabs', - ); - $tests[] = array( - "internal whitespace removed", - 'internal whitespace removed', - ); - - foreach ( $tests as $data ) { - $this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'text' ) ) ); - $this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'custom-field-type' ) ) ); - } - - } - - /** - * Test sanitize_field() for fields whose values are arrays. - * - * @since 5.0.0 - * - * @return void - */ - /*public function test_sanitize_field_arra)y() { - - $tests = $this->data_for_text_fields(; - $t - }*/ - - /** - * Sanitize email fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_email() { - - $emails = array( - "hello'hi@hello.com" => "hello'hi@hello.com", - 'hello@hello.com' => 'hello@hello.com', - 'admin@hello.net' => ' admin@hello.net', - 'admin+hi@hello.edu' => 'admin+hi@hello.edu', - 'hello@so.many.subdomains.org' => 'hello@so.many.subdomains.org', - 'ip@204.32.111.32' => 'ip@204.32.111.32', - 'l@l.ms' => 'l@l.ms', - '' => 'fake', - ); - - foreach ( $emails as $clean => $dirty ) { - $this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'email' ) ) ); - } - - $this->assertEquals( - array( 'hello@hello.com', 'l@l.ms', '' ), - $this->main->sanitize_field( array( 'hello@hello.com', 'l@l.ms', 'j' ), $this->get_field_arr( 'email' ) ) - ); - - } - - /** - * Test email validation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_email() { - - $emails = array( - array( true, "hello'hi@hello.com" ), - array( true, 'hello@hello.com' ), - array( true, 'admin@hello.net' ), - array( true, 'admin+hi@hello.edu' ), - array( true, 'hello@so.many.subdomains.org' ), - array( true, 'ip@204.32.111.32' ), - array( true, 'l@l.ms' ), - array( false, 'fake' ), - array( false, 'f@k.e' ), - array( false, ' f' ), - array( false, 'fake.mock.com' ), - array( true, array( 'ip@204.32.111.32', 'hello@hello.com' ) ), - array( false, array( 'ip@204.32.111.32', 'hello@hello.com', ' f' ) ), - ); - - foreach ( $emails as $test ) { - - $valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'email' ) ); - - if ( $test[0] ) { - $this->assertTrue( $valid ); - } else { - $this->assertIsWPError( $valid ); - } - - } - - } - - /** - * Test validate_field_attribute_minlength() - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_attribute_minlength() { - - $length = 1; - while ( $length <= 25 ) { - - $field = $this->get_field_arr( 'password', array( - 'attributes' => array( - 'minlength' => $length, - ), - ) ); - - // Too short. - $value = str_repeat( 'A', $length - 1 ); - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) ); - $this->assertIsWPError( $res ); - - // Equal - $value .= 'A'; - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) ); - $this->assertTrue( $res ); - - // Longer - $value .= 'AAAA'; - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) ); - $this->assertTrue( $res ); - - ++$length; - - } - - } - - /** - * Test validate_field() for a field with an html minlength attribute - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_minlength_attribute() { - - $field = $this->get_field_arr( 'password', array( - 'attributes' => array( - 'minlength' => 6, - ), - ) ); - - $tests = array( - array( false, 'short' ), - array( false, array( 'short' ) ), - array( false, array( 'short', 'corto' ) ), - array( true, 'it is good' ), - array( true, array( 'it is good' ) ), - array( true, array( 'it is good', 'è buono' ) ), - ); - - foreach ( $tests as $test ) { - $res = $this->main->validate_field( $test[1], $field ); - if ( $test[0] ) { - $this->assertTrue( $res ); - } else { - $this->assertIsWPError( $res ); - } - } - - } - - /** - * Test special validation for the current password field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_password_current() { - - // Not logged in. - $no_user = $this->main->validate_field( 'password', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) ); - $this->assertIsWPError( $no_user ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid-no-user', $no_user ); - - wp_set_current_user( $this->factory->user->create( array( 'user_pass' => 'password' ) ) ); - - // Invalid password. - $invalid = $this->main->validate_field( 'fake', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) ); - $this->assertIsWPError( $invalid ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $invalid ); - - // Valid. - $valid = $this->main->validate_field( 'password', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) ); - $this->assertTrue( $valid ); - - } - - /** - * Test special validation for user emails. They must be unique. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_user_email() { - - $email = sprintf( 'mock+%s@mock.tld', uniqid() ); - - // Valid. - $valid = $this->main->validate_field( $email, $this->get_field_arr( 'email', array( 'id' => 'user_email' ) ) ); - $this->assertTrue( $valid ); - - // Not unique. - $this->factory->user->create( array( 'user_email' => $email ) ); - $exists = $this->main->validate_field( $email, $this->get_field_arr( 'email', array( 'id' => 'user_email' ) ) ); - $this->assertIsWPError( $exists ); - $this->assertWPErrorCodeEquals( 'llms-form-field-not-unique', $exists ); - - } - - /** - * Test special validation for the username field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_user_login() { - - // Banned. - $banned = $this->main->validate_field( 'admin', $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) ); - $this->assertIsWPError( $banned ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $banned ); - - // Not valid. - $invalid = $this->main->validate_field( ' +-!', $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) ); - $this->assertIsWPError( $invalid ); - $this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $invalid ); - - $login = sprintf( 'mock-%s', uniqid() ); - - // Valid. - $valid = $this->main->validate_field( $login, $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) ); - $this->assertTrue( $valid ); - - // Not unique. - $this->factory->user->create( array( 'user_login' => $login ) ); - $exists = $this->main->validate_field( $login, $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) ); - $this->assertIsWPError( $exists ); - $this->assertWPErrorCodeEquals( 'llms-form-field-not-unique', $exists ); - - } - - /** - * Sanitize telephone fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_tel() { - - $tels = array( - '+00 000 000 0000' => '+00 000 000 0000', - '+00-000-000-0000' => '+00-000-000-0000', - '(000) 000 0000' => '(000) 000 0000', - '+00.000.000.0000' => '+00.000.000.0000', - '+00 (000) 000 0000' => '+00 (000) 000 0000', - '000 000 0000 #000' => '000 000 0000 #000', - '+00' => '+00 aaa bbb cccc', - '' => 'fake', - ); - - foreach ( $tels as $clean => $dirty ) { - $this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'tel' ) ) ); - } - - $this->assertEquals( - array( '000 000 0000 #000', '+00' ), - $this->main->sanitize_field( array( '000 000 0000 #000', '+00 aaa bbb cccc' ), $this->get_field_arr( 'tel' ) ) - ); - - } - - /** - * Test telephone validation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_tel() { - - $emails = array( - array( true, '+00 000 000 0000' ), - array( true, '+00-000-000-0000' ), - array( true, '(000) 000 0000' ), - array( true, '+00.000.000.0000' ), - array( true, '+00 (000) 000 0000' ), - array( true, '000 000 0000 #000' ), - array( false, '+00 aaa bbb cccc' ), - array( false, 'fake' ), - array( true, array( '000 000 0000 #000', '(000) 000 0000' ) ), - array( false, array( '000 000 0000 #000', '+00 aaa bbb cccc' ) ), - ); - - foreach ( $emails as $test ) { - - $valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'tel' ) ); - - if ( $test[0] ) { - $this->assertTrue( $valid ); - } else { - $this->assertIsWPError( $valid ); - } - - } - - } - - /** - * Test sanitize_field() for textareas - * - * We don't need super thorough tests here as we're using a WP Core function. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_textarea() { - - $tests = $this->data_for_text_fields(); - - $tests[] = array( - "newlines \nokay", - "newlines \nokay", - ); - $tests[] = array( - "tabs \nokay too", - "tabs \nokay too", - ); - $tests[] = array( - "internal whitespace okay", - "internal whitespace okay", - ); - $tests[] = array( - array( - "internal whitespace okay", - "tabs \nokay too", - ), - array( - "internal whitespace okay", - "tabs \nokay too", - ), - ); - - foreach ( $tests as $data ) { - $this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'textarea' ) ) ); - } - - } - - /** - * Test sanitize_field() for URL fields. - * - * We don't need super thorough tests here as we're using a WP Core function. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_url() { - - $tests = array( - array( - 'https://example.tld/', - 'https://example.tld/', - ), - array( - 'http://www.example.tld', - 'http://www.example.tld', - ), - array( - 'https://example.tld/path/to/something', - 'https://example.tld/path/to/something', - ), - array( - 'https://example.tld?qs=yes&more=1', - 'https://example.tld?qs=yes&more=1', - ), - array( - 'https://example.tld/with space', - 'https://example.tld/with%20space', - ), - array( - 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D', - '', - ), - array( - 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D', - '', - ), - array( - 'https://example.tld/?qs=whatever+<script>alert(1)</script>', - 'https://example.tld/?qs=whatever+scriptalert(1)/script', - ), - array( - array( - 'http://www.example.tld', - 'https://example.tld/?qs=whatever+<script>alert(1)</script>', - ), - array( - 'http://www.example.tld', - 'https://example.tld/?qs=whatever+scriptalert(1)/script', - ), - ), - ); - - foreach ( $tests as $data ) { - $this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'url' ) ) ); - } - - } - - /** - * Test validate_field() for a URL field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_url() { - - $tests = array( - array( false, 'notaurl' ), - array( false, 'test.php' ), - array( false, 'example.tld' ), - array( true, 'https://example.tld' ), - array( true, 'https://example.tld' ), - array( true, array( 'https://example.tld', 'https://another-example.ltd' ) ), - array( false, array( 'https://example.tld', 'another-example.ltd' ) ), - ); - - foreach ( $tests as $test ) { - - $valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'url' ) ); - - if ( $test[0] ) { - $this->assertTrue( $valid ); - } else { - $this->assertIsWPError( $valid ); - } - - } - - } - - /** - * Sanitize number fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_sanitize_field_for_number() { - - $numbers = array( - '1' => '1', - '100' => '100', - '1.00' => '1.00', - '1,00' => '1,00', - '1,000' => '1,000', - '1.000' => '1.000', - '1,000.00' => '1,000.00', - '1.000,00' => '1.000,00', - '2' => ' fake 2 mock', - ); - - foreach ( $numbers as $clean => $dirty ) { - $this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'number' ) ) ); - } - - $this->assertEquals( - array( '1', '100' ), - $this->main->sanitize_field( array( '1', '100' ), $this->get_field_arr( 'number' ) ) - ); - - $this->assertEquals( - array( '1', '100', '2' ), - $this->main->sanitize_field( array( '1', '100', ' fake 2 mock' ), $this->get_field_arr( 'number' ) ) - ); - - } - - /** - * Test number field validation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_number() { - - $field = $this->get_field_arr( 'number', array( 'name' => 'number_field' ) ); - - $tests = array( - array( true, '1' ), - array( true, '-1' ), - array( true, '+1' ), - array( true, '100' ), - array( true, '1.00' ), - array( true, '1,00' ), - array( true, '1,000' ), - array( true, '1.000' ), - array( true, '1,000.00' ), - array( true, '1.000,00' ), - array( false, ' fake 2 mock' ), - array( true, array( '1.000,00', '1' ) ), - array( false, array( '1.000,00', '1', ' fake 2 mock' ) ), - ); - - foreach ( $tests as $test ) { - - $valid = $this->main->validate_field( $test[1], $field ); - - if ( $test[0] ) { - $this->assertTrue( $valid ); - } else { - $this->assertIsWPError( $valid ); - } - - } - - $field['attributes']['min'] = '25'; - $field['attributes']['max'] = '500'; - - $tests = array( - array( 'greater', '10' ), - array( 'greater', '10.50' ), - array( 'greater', '24.99' ), - array( 'greater', '-500' ), - array( 'less', '500.01' ), - array( 'less', '1,000' ), - array( 'less', '+99999' ), - array( 'less', '909090' ), - ); - foreach ( $tests as $test ) { - - $res = $this->main->validate_field( $test[1], $field ); - $this->assertIsWPError( $res ); - $this->assertStringContains( $test[0], $res->get_error_message() ); - - } - - } - - /** - * Test number field validation with empty limits - * - * When min|max attributes are set but empty (like empty string): default. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_number_with_empty_limits() { - - $field = $this->get_field_arr( 'number', array( 'name' => 'number_field' ) ); - - $field['attributes']['min'] = ''; - $field['attributes']['max'] = ''; - - $this->assertTrue( $this->main->validate_field( '1', $field ) ); - $this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) ); - - $field['attributes']['min'] = '0'; - $field['attributes']['max'] = ''; - - $this->assertTrue( $this->main->validate_field( '1', $field ) ); - $this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) ); - $this->assertIsWPError( $this->main->validate_field( '-1', $field ) ); - $this->assertStringContains( 'greater', $this->main->validate_field( '-1', $field )->get_error_message() ); - - $field['attributes']['min'] = ''; - $field['attributes']['max'] = '5'; - - $this->assertTrue( $this->main->validate_field( '1', $field ) ); - $this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) ); - $this->assertIsWPError( $this->main->validate_field( '6', $field ) ); - $this->assertStringContains( 'less', $this->main->validate_field( '6', $field )->get_error_message() ); - - } - - /** - * Test special voucher field validation. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_field_for_voucher() { - - $field = $this->get_field_arr( 'text', array( 'id' => 'llms_voucher' ) ); - - // Invalid code. - $res = $this->main->validate_field( 'invalid-code', $field ); - $this->assertIsWPError( $res ); - $this->assertWPErrorMessageEquals( 'Voucher code "invalid-code" could not be found.', $res ); - - // Valid code. - $voucher = $this->get_mock_voucher( 1 ); - $code = $voucher->get_voucher_codes()[0]->code; - $res = $this->main->validate_field( $code, $field ); - $this->assertTrue( $res ); - - // Use the voucher. - $voucher->use_voucher( $code, 123 ); - - // Valid code without any remaining redemptions. - $res = $this->main->validate_field( $code, $field ); - $this->assertIsWPError( $res ); - $this->assertWPErrorMessageEquals( sprintf( 'Voucher code "%s" has already been redeemed the maximum number of times.', $code ), $res ); - - } - - /** - * Test checking matching fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_matching_fields() { - - $posted = array( - 'email' => 'l@l.ms', - 'email_confirm' => 'l@l.ms', - ); - - $fields = array( - array( - 'id' => 'email', - 'name' => 'email', - 'match' => 'email_confirm', - ), - array( - 'id' => 'email_confirm', - 'name' => 'email_confirm', - 'match' => 'email', - ), - ); - - $this->assertTrue( $this->main->validate_matching_fields( $posted, $fields ) ); - - } - - /** - * Test checking matching fields when the matching field doesn't exist in the form. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_matching_fields_err_missing_match_definition() { - - $posted = array( - 'email' => 'l@l.ms', - ); - - $fields = array( - array( - 'id' => 'email', - 'name' => 'email', - 'match' => 'email_confirm', - ), - ); - - $this->assertTrue( $this->main->validate_matching_fields( $posted, $fields ) ); - - } - - /** - * Test checking matchind fields when user data is mismatched. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_matching_fields_err_missing_match() { - - $posted = array( - 'email' => 'l@l.ms', - ); - - $fields = array( - array( - 'id' => 'email', - 'name' => 'email', - 'match' => 'email_confirm', - ), - array( - 'id' => 'email_confirm', - 'name' => 'email_confirm', - 'match' => 'email', - ), - ); - - $valid = $this->main->validate_matching_fields( $posted, $fields ); - $this->assertIsWPError( $valid ); - $this->assertWPErrorCodeEquals( 'llms-form-field-not-matched', $valid ); - - // Should only have a single error message, not one error for each message. - $this->assertEquals( 1, count( $valid->get_error_messages( 'llms-form-field-not-matched' ) ) ); - - } - - /** - * Test validate_required_fields() - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_required_fields_exist() { - - $posted = array( - 'email' => 'fake@mock.com', - 'password' => '1234', - ); - - $fields = array( - array( - 'label' => __( 'Email', 'lifterlms' ), - 'name' => 'email', - 'required' => true, - ), - array( - 'label' => __( 'Password', 'lifterlms' ), - 'name' => 'password', - 'required' => true, - ), - array( - 'label' => __( 'Name', 'lifterlms' ), - 'name' => 'name', - 'required' => false, - ), - ); - - $this->assertTrue( $this->main->validate_required_fields( $posted, $fields ) ); - - } - - /** - * Test validity of form based on presence of all required fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_validate_required_fields_missing_fields() { - - $posted = array(); - - $fields = array( - array( - 'label' => __( 'Email', 'lifterlms' ), - 'name' => 'email', - 'required' => true, - ), - array( - 'label' => __( 'Password', 'lifterlms' ), - 'name' => 'password', - 'required' => true, - ), - array( - 'label' => __( 'Name', 'lifterlms' ), - 'name' => 'name', - 'required' => false, - ), - ); - - // Missing both required fields, - $valid = $this->main->validate_required_fields( $posted, $fields ); - $this->assertIsWPError( $valid ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $valid ); - $this->assertEquals( 2, count( $valid->errors['llms-form-missing-required'] ) ); - $this->assertEquals( 'Email is a required field.', $valid->errors['llms-form-missing-required'][0] ); - $this->assertEquals( 'Password is a required field.', $valid->errors['llms-form-missing-required'][1] ); - - // Only missing password. - $posted['email'] = 'fake@mock.com'; - $valid = $this->main->validate_required_fields( $posted, $fields ); - $this->assertIsWPError( $valid ); - $this->assertWPErrorCodeEquals( 'llms-form-missing-required', $valid ); - $this->assertEquals( 1, count( $valid->errors['llms-form-missing-required'] ) ); - $this->assertEquals( 'Password is a required field.', $valid->errors['llms-form-missing-required'][0] ); - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms-admin-bar.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms-admin-bar.php deleted file mode 100644 index bd601925c0..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms-admin-bar.php +++ /dev/null @@ -1,159 +0,0 @@ -<?php -/** - * Test LLMS_Forms_Admin_Bar class - * - * @package LifterLMS/Tests - * - * @group forms - * @group forms_admin_bar - * - * @since 5.0.0 - */ -class LLMS_Test_Forms_Admin_Bar extends LLMS_UnitTestCase { - - /** - * Setup the test - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Forms_Admin_Bar(); - - } - - /** - * Initiate (and retrieve) an instance of WP_Admin_Bar - * - * @since 5.0.0 - * - * @return WP_Admin_Bar - */ - private function get_admin_bar() { - - add_filter( 'show_admin_bar', '__return_true' ); - _wp_admin_bar_init(); - - global $wp_admin_bar; - - remove_filter( 'show_admin_bar', '__return_true' ); - - return $wp_admin_bar; - - } - - /** - * Test add_menu_items() when nothing should be added - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_menu_items_no_display() { - - $bar = $this->get_admin_bar(); - - $this->main->add_menu_items( $bar ); - - $this->assertNull( $bar->get_nodes() ); - - } - - /** - * Test add_menu_items() - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_menu_items() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Forms::instance()->install(); - LLMS_Install::create_pages(); - $this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) ); - - $bar = $this->get_admin_bar(); - - add_filter( 'llms_view_manager_should_display', '__return_true' ); - - $this->main->add_menu_items( $bar ); - - $this->assertEquals( array( 'llms-edit-form' ), array_keys( $bar->get_nodes() ) ); - - remove_filter( 'llms_view_manager_should_display', '__return_true' ); - - } - - /** - * Test get_current_location() - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_current_location() { - - // Invalid screen. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - LLMS_Install::create_pages(); - - // Checkout. - $this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) ); - $this->assertEquals( 'checkout', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) ); - - // Edit Account. - $this->go_to( llms_person_edit_account_url() ); - $this->assertEquals( 'account', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) ); - - // Open Reg. - update_option( 'lifterlms_enable_myaccount_registration', 'yes' ); - $this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) ); - $this->mockGetRequest( array( 'llms-view-as' => 'visitor' ) ); - $this->assertEquals( 'registration', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) ); - - - } - - /** - * Test should_display() on checkout page - * - * @since 5.0.0 - * - * @return void - */ - public function test_should_display() { - - // No user. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Invalid screen. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - LLMS_Install::create_pages(); - - // Checkout. - $this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - // Edit Account. - $this->go_to( llms_person_edit_account_url() ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - // Open Reg. - update_option( 'lifterlms_enable_myaccount_registration', 'yes' ); - $this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) ); - $this->mockGetRequest( array( 'llms-view-as' => 'visitor' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms-classic-editor.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms-classic-editor.php deleted file mode 100644 index 5856f1ac2c..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms-classic-editor.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php -/** - * Test LLMS_Forms_Classic_Editor class - * - * @package LifterLMS/Tests - * - * @group forms - * @group forms_classic - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Forms_Classic_Editor extends LLMS_UnitTestCase { - - /** - * Test init() - * - * @since 5.0.0 - * - * @return void - */ - public function test_init() { - - remove_filter( 'use_block_editor_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'force_block_editor' ), 200 ); - remove_filter( 'classic_editor_enabled_editors_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'disable_classic_editor' ), 20 ); - - LLMS_Forms_Classic_Editor::init(); - - $this->assertEquals( 200, has_filter( 'use_block_editor_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'force_block_editor' ) ) ); - $this->assertEquals( 20, has_filter( 'classic_editor_enabled_editors_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'disable_classic_editor' ) ) ); - - } - - /** - * Test force_block_editor() - * - * @since 5.0.0 - * - * @return void - */ - public function test_force_block_editor() { - - $tests = array( - 'post' => array( - array( - 'input' => true, - 'output' => true - ), - array( - 'input' => false, - 'output' => false, - ) - ), - 'page' => array( - array( - 'input' => true, - 'output' => true - ), - array( - 'input' => false, - 'output' => false, - ) - ), - 'course' => array( - array( - 'input' => true, - 'output' => true - ), - array( - 'input' => false, - 'output' => false, - ) - ), - 'llms_form' => array( - array( - 'input' => true, - 'output' => true - ), - array( - 'input' => false, - 'output' => true, - ) - ), - ); - - foreach ( $tests as $post_type => $groups ) { - foreach ( $groups as $data ) { - $this->assertSame( $data['output'], LLMS_Forms_Classic_Editor::force_block_editor( $data['input'], $post_type ) ); - } - } - - } - - /** - * Test disable_classic_editor() - * - * @since 5.0.0 - * - * @return void - */ - public function test_disable_classic_editor() { - - $expected = array( - 'classic_editor' => true, - 'block_editor' => true, - ); - foreach ( array( 'post', 'page', 'course' ) as $post_type ) { - $this->assertEquals( $expected, LLMS_Forms_Classic_Editor::disable_classic_editor( $expected, $post_type ) ); - } - - $expected['classic_editor'] = false; - $this->assertEquals( $expected, LLMS_Forms_Classic_Editor::disable_classic_editor( $expected, 'llms_form' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms-data.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms-data.php deleted file mode 100644 index 7f72b58aaf..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms-data.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Test LLMS_Forms Singleton - * - * @package LifterLMS/Tests - * - * @group forms_data - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Forms_Data extends LLMS_UnitTestCase { - - /** - * Setup the test - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Forms_Data(); - $this->forms = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'forms' ); - - } - - public function test_constructor() { - - remove_action( 'save_post_llms_form', array( $this->main, 'save_username_locations' ), 10 ); - - $this->main = new LLMS_Forms_Data(); - - $this->assertEquals( 10, has_action( 'save_post_llms_form', array( $this->main, 'save_username_locations' ) ) ); - - $this->assertTrue( is_a( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'forms' ), 'LLMS_Forms' ) ); - - } - - public function test_save_username_location_with_username() { - - update_option( 'lifterlms_registration_generate_username', 'no' ); - $reg_form_id = $this->forms->create( 'registration', true ); - $expect = array( $reg_form_id ); - - // Clear data. - delete_option( 'llms_forms_username_locations' ); - - $res = $this->main->save_username_locations( $reg_form_id, get_post( $reg_form_id ) ); - $this->assertEquals( $expect, $res ); - - // Add another form. - $form_id = $this->forms->create( 'checkout', true ); - $expect[] = $form_id; - $res = $this->main->save_username_locations( $form_id, get_post( $form_id ) ); - $this->assertEquals( $expect, $res ); - - // Recreate the form without a username field, this should remove the form id. - delete_option( 'lifterlms_registration_generate_username', 'no' ); - $form_id = $this->forms->create( 'checkout', true ); - $res = $this->main->save_username_locations( $form_id, get_post( $form_id ) ); - $this->assertEquals( array( $reg_form_id ), $res ); - - } - -} - - - - diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms-dynamic-fields.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms-dynamic-fields.php deleted file mode 100644 index 34feece945..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms-dynamic-fields.php +++ /dev/null @@ -1,644 +0,0 @@ -<?php -/** - * Test LLMS_Forms_Dynamic_Fields Singleton - * - * @package LifterLMS/Tests - * - * @group forms - * @group forms_dynamic_fields - * - * @since 5.0.0 - * @version 5.4.1 - */ -class LLMS_Test_Forms_Dynamic_fields extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = new LLMS_Forms_Dynamic_fields(); - $this->forms = LLMS_Forms::instance(); - } - - /** - * Test add_password_strength_meter() when no password field found - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_password_strength_meter_no_password() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-text {"id":"block-one"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) ); - $this->assertEquals( $blocks, $res ); - } - - /** - * Test add_password_strength_meter() when the password meter attr is not present - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_password_strength_meter_meter_attr_not_present() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {"id":"password"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) ); - $this->assertEquals( $blocks, $res ); - - } - - /** - * Test add_password_strength_meter() when the meter is explicitly disabled - * - * @since 5.0.0 - * - * @return void - */ - public function test_add_password_strength_meter_meter_disabled() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {"id":"password","meter":"no"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) ); - $this->assertEquals( $blocks, $res ); - - } - - /** - * Test add_password_strength_meter() when meter is enabled - * - * @since 5.0.0 - * @since 5.0.1 Add aria attribute to expected response. - * - * @return void - */ - public function test_add_password_strength_meter_meter_enabled() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {"id":"password","meter":"yes","meter_description":"test"} /--><!-- wp:llms/form-field-text {"id":"block-one"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) ); - - // Password block is unaffected. - $this->assertEquals( $blocks[0], $res[0] ); - - $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'] ) ); - - // Block after password is in the new last position, unaffected. - $this->assertEquals( $blocks[1], $res[2] ); - - } - - - /** - * Test add_password_strength_meter() when meter is enabled on the account edit screen - * - * @since 5.0.0 - * @since 5.0.1 Add aria attribute to expected response. - * - * @return void - */ - public function test_add_password_strength_meter_meter_enabled_account() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {"id":"password","meter":"yes","meter_description":"test"} /--><!-- wp:llms/form-field-text {"id":"block-one"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'account' ) ); - - // Password block is unaffected. - $this->assertEquals( $blocks[0], $res[0] ); - - // Differs from above test because of the `llms-visually-hidden-field` class. - $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'] ) ); - - // Block after password is in the new last position, unaffected. - $this->assertEquals( $blocks[1], $res[2] ); - - } - - /** - * Test find_block() when no password confirmation is present - * - * This also tests that the password field isn't the first field in the form to ensure the index returns properly. - * - * @since 5.0.0 - * - * @return void - */ - public function test_find_block_not_nested() { - - $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"} /-->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) ); - - $this->assertEquals( array( 1, $blocks[1] ), $res ); - - } - - /** - * Test find_block() when no password block is present - * - * @since 5.0.0 - * - * @return void - */ - public function test_find_block_no_field() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-text {"id":"block-one"} /-->' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) ) ); - - } - - /** - * Test find_block() when a password confirm field is used - * - * @since 5.0.0 - * - * @return void - */ - public function test_find_block_with_confirm() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-confirm-group --> -<!-- wp:llms/form-field-user-password {"id":"password"} /--> -<!-- wp:llms/form-field-text {"id":"password-confirm"} /--> -<!-- /wp:llms/form-field-confirm-group -->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) ); - - $this->assertEquals( array( 0, $blocks[0]['innerBlocks'][0] ), $res ); - - } - - /** - * 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) - * - * @since 5.0.0 - * - * @return void - */ - public function test_find_block_nested() { - - $blocks = parse_blocks( '<!-- wp:group --> -<div class="wp-block-group"><div class="wp-block-group__inner-container"><!-- wp:llms/form-field-confirm-group --> -<!-- wp:llms/form-field-user-password {"id":"password"} /--> -<!-- wp:llms/form-field-text {"id":"password-confirm"} /--> -<!-- /wp:llms/form-field-confirm-group --></div></div> -<!-- /wp:group -->' ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) ); - - $this->assertEquals( array( 0, $blocks[0]['innerBlocks'][0]['innerBlocks'][0] ), $res ); - - } - - public function test_get_toggle_button_html() { - - $expect = '<a class="llms-toggle-fields" data-fields="#mock" data-change-text="Change Label" data-cancel-text="Cancel" href="#">Change Label</a>'; - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_toggle_button_html', array( '#mock', 'Label' ) ); - - $this->assertEquals( $expect, $res ); - - } - - public function test_modify_account_form_wrong_form() { - - $input = 'fake'; - $this->assertEquals( $input, $this->main->modify_account_form( $input, 'checkout' ) ); - - } - - public function test_modify_account_form() { - - $fields = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( parse_blocks( LLMS_Form_Templates::get_template( 'account' ) ) ) ); - - - $res = $this->main->modify_account_form( $fields, 'account' ); - - // @todo. - - } - - /** - * Test required fields block added to form blocks - * - * @since 5.1.0 - * - * @return void - */ - public function test_maybe_add_required_block_fields() { - - // Make sure no user is logged in. - wp_set_current_user( null ); - - // Email and pw fields not added to forms which are not checkout or registration. - $this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'what', array() ) ); - $this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'account', array() ) ); - - // Email and pw fields added to checkout form. - $checkout_blocks = $this->main->maybe_add_required_block_fields( array(), 'checkout', array() ); - foreach ( array( 'email_address', 'password' ) as $id ) { - $this->assertNotEmpty( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $checkout_blocks - ) - ), - $id - ); - } - - // Email and pw fields added to registration form. - $registration_blocks = $this->main->maybe_add_required_block_fields( array(), 'registration', array() ); - foreach ( array( 'email_address', 'password' ) as $id ) { - $this->assertNotEmpty( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $registration_blocks - ) - ), - $id - ); - } - - // Log in. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - // Email and pw field not added to any forms except account for logged in users. - $this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'what', array() ) ); - $this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'checkout', array() ) ); - $this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'registration', array() ) ); - - $account_blocks = $this->main->maybe_add_required_block_fields( array(), 'account', array() ); - foreach ( array( 'email_address', 'password' ) as $id ) { - $this->assertNotEmpty( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $account_blocks - ) - ), - $id - ); - } - - // Make sure no user is logged in. - wp_set_current_user( null ); - - } - - /** - * Test required fields made visible if they were not - * - * This additionally covers `LLMS_Forms_Dynamic_Fields::make_block_visible()` and `LLMS_Forms_Dynamic_Fields::get_confirm_group()`. - * - * @since 5.1.1 - * - * @return void - */ - public function test_maybe_add_required_block_fields_not_visible_fields() { - - // Make sure no user is logged in. - wp_set_current_user( null ); - - // Only visible to logged in users, for testing purposes. - $email_confirm_original_block = array( // Use a confirm block to cover `LLMS_Forms_Dynamic_Fields::get_confirm_group()`. - 'blockName' => 'llms/form-field-confirm-group', - 'attrs' => array( - 'fieldLayout' => 'columns', - 'llms_visibility' => 'logged_in', - ), - 'innerBlocks' => array( - array( - 'blockName' => 'llms/form-field-user-email', - 'attrs' => array( - 'required' => true, - 'llms_visibility' => 'logged_in', - 'id' => 'email_address', - 'name' => 'email_address', - 'label' => 'Email Address', - 'data_store' => 'users', - 'data_store_key' => 'user_email', - 'field' => 'email', - 'isConfimationControlField' => true, - 'match' => 'email_address_confirm', - 'isOriginal' => true, // For testing purposes. - ), - 'innerBlocks' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - ), - array( - 'blockName' => 'llms/form-field-text', - 'attrs' => array( - 'required' => true, - 'id' => 'email_address_confirm', - 'name' => 'email_address_confirm', - 'label' => 'Confirm Email Address', - 'data_store' => '', - 'data_store_key' => '', - 'field' => 'email', - 'isConfimationControlField' => true, - 'match' => 'email_address_confirm', - ), - 'innerBlocks' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - ), - ), - 'innerHTML' => '', - 'innerContent' => array( - null, - null, - ), - ); - - // Only visible to logged in users, for testing purposes. - $password_original_block = array( - 'blockName' => 'llms/form-field-user-password', - 'attrs' => array( - 'required' => true, - 'llms_visibility' => 'logged_in', - 'id' => 'password', - 'name' => 'password', - 'label' => 'Password', - 'data_store' => 'users', - 'data_store_key' => 'user_pass', - 'field' => 'password', - 'isOriginal' => true, // For testing purposes. - ), - 'innerBlocks' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - ); - - $blocks = $this->main->maybe_add_required_block_fields( - array( - $email_confirm_original_block, - $password_original_block - ), - 'checkout', - array() - ); - - - // Check the email block is visible. - $email_block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - 'email_address', - $blocks - ) - ); - $this->assertNotEmpty( $email_block ); - $this->assertTrue( $this->forms->is_block_visible_in_list( $email_block[1], $blocks ) ); - - // Check the password is visible. - $password_block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - 'password', - $blocks - ) - ); - $this->assertNotEmpty( $password_block ); - $this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) ); - - // Check both email and password block are the original ones (not replaced, only made visibile). - $this->assertTrue( $email_block[1]['attrs']['isOriginal'] ); - $this->assertTrue( $password_block[1]['attrs']['isOriginal'] ); - - // Move the password block into a group block, so to test it's correctly extrapolated from its parent. - $blocks = $this->main->maybe_add_required_block_fields( - array( - $email_confirm_original_block, - array( - 'blockName' => 'core/group', - 'attrs' => array( - 'isPasswordParent' => true, - ), - 'innerBlocks' => array( - $password_original_block - ), - 'innerHTML' => '', - 'innerContent' => array( - null - ), - ) - ), - 'checkout', - array() - ); - - // Check the password is visible. - $password_block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - 'password', - $blocks - ) - ); - $this->assertNotEmpty( $password_block ); - $this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) ); - // It's the original one. - $this->assertTrue( $password_block[1]['attrs']['isOriginal'] ); - // Check it has no parents anymore. - $this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) ); - // Check its former parent is now empty. - foreach ( $blocks as $block ) { - if ( 'core/group' === $block['blockName'] && ! empty( $block['attrs']['isPasswordParent'] ) ) { - $this->assertEmpty( $block['innerBlocks'] ); - } - } - - } - - /** - * Test required fields blocks not added to form blocks if they already have them. - * - * @since 5.1.0 - * - * @return void - */ - public function test_maybe_add_required_block_fields_check_no_dupes() { - - foreach ( array( 'checkout', 'registration', 'account' ) as $location ) { - if ( 'account' === $location ) { - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - } - - $this->forms->create( $location, true ); - $blocks = $this->forms->get_form_blocks( $location ); - - foreach ( array( 'email_address', 'password' ) as $id ) { - - $block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $blocks - ) - ); - $this->assertNotEmpty( - $block, - "{$location}:{$id}" - ); - - // Check again for dupes. - array_splice( $blocks, $block[0], 1); // Remove just found block. - - $this->assertEmpty( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $blocks - ) - ), - "{$location}:{$id}" - ); - } - - if ( 'account' === $location ) { - wp_set_current_user( null ); - } - - } - - } - - /** - * Test remove_block - * - * @since 5.1.0 - * - * @return void - */ - public function test_remove_block() { - - $this->forms->create( 'checkout', true ); - $blocks = $this->forms->get_form_blocks( 'checkout' ); - - // Remove a field block, e.g. the email one. - $email_field_block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - 'email_address', - $blocks - ) - )[1]; - - $removed = LLMS_Unit_Test_Util::call_method( - $this->main, - 'remove_block', - array( - $email_field_block, - &$blocks - ) - ); - - $this->assertTrue( $removed ); - - $this->assertFalse( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - 'email_address', - $blocks - ) - ) - ); - - $this->assertFalse( - LLMS_Unit_Test_Util::call_method( - $this->main, - 'remove_block', - array( - $email_field_block, - &$blocks - ) - ) - ); - - } - - /** - * Test required fields are still added if their reusable blocks exist but do not contain them. - * - * @since 5.4.1 - * - * @return void - */ - public function test_required_fields_added_when_reusable_empty() { - - foreach ( array( 'checkout', 'registration' ) as $location ) { - if ( 'account' === $location ) { - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - } - - // Get reusable blocks. - foreach ( array( 'email', 'password' ) as $block_name ) { - $reusable_block = LLMS_Form_Templates::get_block( $block_name, $location, true ); - - // Turn reusable block contents into text (we remove the fields from them). - if ( ! empty( $reusable_block['attrs']['ref'] ) ) { - wp_update_post( - array( - 'ID' => $reusable_block['attrs']['ref'], - 'post_content' => '<p>Nothing special</p>', - ) - ); - } - } - - $this->forms->create( $location, true ); - // Here's where the required fields are added back. - $blocks = $this->forms->get_form_blocks( $location ); - - foreach ( array( 'email_address', 'password' ) as $id ) { - - $block = LLMS_Unit_Test_Util::call_method( - $this->main, - 'find_block', - array( - $id, - $blocks - ) - ); - $this->assertNotEmpty( - $block, - "{$location}:{$id}" - ); - - } - - if ( 'account' === $location ) { - wp_set_current_user( null ); - } - - } - - } -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms-unsupported-versions.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms-unsupported-versions.php deleted file mode 100644 index 638daa031e..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms-unsupported-versions.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php -/** - * Test LLMS_Forms_Unsupported_Versions - * - * @package LifterLMS/Tests/Forms - * - * @group forms - * @group forms_unsupported_versions - * - * @since 5.0.0 - */ -class LLMS_Test_Forms_Unsupported_Versions extends LLMS_UnitTestCase { - - /** - * Set up before class - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - require_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-unsupported-versions.php'; - } - - /** - * Setup the test case - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->init_main(); - - } - - /** - * Construct a new main class for testing. - * - * @since 5.0.0 - * - * @return void - */ - public function init_main() { - $this->main = new LLMS_Forms_Unsupported_Versions(); - } - - /** - * Test constructor - * - * @since 5.0.0 - * - * @return void - */ - public function test_constructor() { - - global $wp_version; - $temp = $wp_version; - - $versions = array( - '5.8.0' => false, - '5.7.2' => false, - '5.7.0' => false, - '5.6.2' => 10, - '5.6.0' => 10, - '5.5.0' => 10, - ); - - foreach ( $versions as $wp_version => $expect ) { - $this->init_main(); - $this->assertEquals( $expect, has_action( 'current_screen', array( $this->main, 'init' ) ) ); - remove_action( 'current_screen', array( $this->main, 'init' ) ); - } - - $wp_version = $temp; - - } - - /** - * Test init() when nothing should happen - * - * @since 5.0.0 - * - * @return void - */ - public function test_init_for_other() { - - set_current_screen( 'admin.php' ); - - $this->main->init(); - - $this->assertFalse( has_action( 'admin_print_styles', array( $this->main, 'print_styles' ) ) ); - $this->assertFalse( has_action( 'admin_notices', array( $this->main, 'output_notice' ) ) ); - - set_current_screen( 'front' ); - - } - - /** - * Test init() for the forms post table list - * - * @since 5.0.0 - * - * @return void - */ - public function test_init_for_post_table() { - - set_current_screen( 'edit-llms_form' ); - - $this->main->init(); - - $this->assertEquals( 10, has_action( 'admin_print_styles', array( $this->main, 'print_styles' ) ) ); - $this->assertEquals( 10, has_action( 'admin_notices', array( $this->main, 'output_notice' ) ) ); - - set_current_screen( 'front' ); - - } - - /** - * Test init() when accessing a form block editor directly - * - * @since 5.0.0 - * - * @return void - */ - public function test_init_for_form_post() { - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( 'http://example.org/wp-admin/edit.php?post_type=llms_form [302] YES' ); - - try { - - set_current_screen( 'llms_form' ); - $this->main->init(); - - } catch ( LLMS_Unit_Test_Exception_Redirect $exception ) { - - set_current_screen( 'front' ); - throw $exception; - - } - - } -} diff --git a/tests/phpunit/unit-tests/forms/class-llms-test-forms.php b/tests/phpunit/unit-tests/forms/class-llms-test-forms.php deleted file mode 100644 index fce33fce23..0000000000 --- a/tests/phpunit/unit-tests/forms/class-llms-test-forms.php +++ /dev/null @@ -1,1448 +0,0 @@ -<?php -/** - * Test LLMS_Forms Singleton - * - * @package LifterLMS/Tests - * - * @group forms - * - * @since 5.0.0 - * @version 5.1.1 - */ -class LLMS_Test_Forms extends LLMS_UnitTestCase { - - /** - * Setup the test - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->forms = LLMS_Forms::instance(); - - } - - /** - * Teardown the test. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - global $wpdb; - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - } - - /** - * Retrieve an array of form locations to run tests against. - * - * @since 5.0.0 - * - * @return string[] - */ - private function get_form_locs() { - - return array( 'checkout', 'registration', 'account' ); - - } - - /** - * Assert that an array looks like a WordPress block array. - * - * @since 5.0.0 - * - * @param array $block Block settings array. - * @return void - */ - protected function assertIsABlock( $block ) { - - foreach ( array( 'blockName', 'attrs', 'innerBlocks', 'innerHTML', 'innerContent' ) as $prop ) { - $this->assertTrue( array_key_exists( $prop, $block ), "Block is missing property {$prop}." ); - } - - if ( ! empty( $block['innerBlocks'] ) ) { - foreach ( $block['innerBlocks'] as $innerBlock ) { - $this->assertIsABlock( $innerBlock ); - } - } - - } - - /** - * Assert that an array looks like a LifterLMS Form Field settings array. - * - * @since 5.0.0 - * - * @param array $field Field settings array. - * @return void - */ - protected function assertIsAField( $field ) { - - foreach ( array( 'id', 'name', 'type' ) as $prop ) { - $this->assertTrue( array_key_exists( $prop, $field ), "Field is missing property {$prop}." ); - } - - } - - /** - * Test singleton instance. - * - * @since 5.0.0 - * - * @return void - */ - public function test_instance() { - - $this->assertClassHasStaticAttribute( 'instance', 'LLMS_Forms' ); - - } - - /** - * Test are_requirements_met() - * - * @since 5.0.0 - * - * @return void - */ - public function test_are_requirements_met() { - - global $wp_version; - $temp = $wp_version; - - $versions = array( - '5.3.1' => false, - '5.6.0' => false, - '5.6.5' => false, - '5.7.0' => true, - '5.7.2' => true, - '5.8.0' => true, - ); - - foreach ( $versions as $wp_version => $expect ) { - - $this->assertEquals( $expect, LLMS_Forms::instance()->are_requirements_met(), $wp_version ); - - } - - // Restore the version. - $wp_version = $temp; - - } - - /** - * Test are_usernames_enabled() when at least one form with a username block exists. - * - * @since 5.0.0 - * - * @return void - */ - public function test_are_usernames_enabled_one_form_with_usernames() { - - update_option( 'lifterlms_registration_generate_username', 'no' ); - $this->forms->create( 'registration', true ); - - $this->assertTrue( $this->forms->are_usernames_enabled() ); - - // Explicitly disabled by the filter. - add_filter( 'llms_are_usernames_enabled', '__return_false' ); - $this->assertFalse( $this->forms->are_usernames_enabled() ); - remove_filter( 'llms_are_usernames_enabled', '__return_false' ); - - } - - /** - * Test are_usernames_enabled() when no forms with usernames exist. - * - * @since 5.0.0 - * - * @return void - */ - public function test_are_usernames_enabled_no_forms_with_usernames() { - - update_option( 'lifterlms_registration_generate_username', 'yes' ); - $this->forms->create( 'registration', true ); - $this->forms->create( 'checkout', true ); - - $this->assertFalse( $this->forms->are_usernames_enabled() ); - - // Explicitly enabled by the filter. - add_filter( 'llms_are_usernames_enabled', '__return_true' ); - $this->assertTrue( $this->forms->are_usernames_enabled() ); - remove_filter( 'llms_are_usernames_enabled', '__return_true' ); - - } - - /** - * Test are_usernames_enabled() when there's a mixture of forms with and without usernames. - * - * @since 5.0.0 - * - * @return void - */ - public function test_are_usernames_enabled_some_forms_with_usernames() { - - // Has username. - update_option( 'lifterlms_registration_generate_username', 'no' ); - $this->forms->create( 'checkout', true ); - - // Doesn't have username. - update_option( 'lifterlms_registration_generate_username', 'yes' ); - $this->forms->create( 'registration', true ); - - $this->assertTrue( $this->forms->are_usernames_enabled() ); - - } - - /** - * Test block_to_field_settings(): ensure keys are renamed properly. - * - * @since 5.0.0 - * - * @return void - */ - public function test_block_to_field_settings() { - - $attrs = array( - 'id' => 'field_id', - 'className' => 'mock fake class-name', - 'field' => 'text', - 'extra' => 'remains', - ); - $html = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) ); - $blocks = parse_blocks( $html ); - - $parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) ); - $expect = array( - 'id' => 'field_id', - 'classes' => 'mock fake class-name', - 'type' => 'text', - 'extra' => 'remains', - ); - $this->assertEquals( $expect, $parsed ); - - } - - /** - * Test block_to_field_settings(): no keys to rename so attributes don't change. - * - * @since 5.0.0 - * - * @return void - */ - public function test_block_to_field_settings_no_updates() { - - $attrs = array( - 'id' => 'field_id', - 'extra' => 'remains', - ); - $html = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) ); - $blocks = parse_blocks( $html ); - - $parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) ); - $this->assertEquals( $attrs, $parsed ); - - } - - /** - * Test block_to_field_settings(): has visibility but the field isn't required so we don't do anything. - * - * @since 5.0.0 - * - * @return void - */ - public function test_block_to_field_settings_with_visiblity_no_required() { - - $attrs = array( - 'id' => 'field_id', - 'llms_visibility' => 'logged_in', - 'extra' => 'remains', - ); - $html = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) ); - $blocks = parse_blocks( $html ); - - $parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) ); - $this->assertEquals( $attrs, $parsed ); - - } - - /** - * Test block_to_field_settings(): has visibility and field is required so the required should be switched to optional. - * - * @since 5.0.0 - * - * @return void - */ - public function test_block_to_field_settings_with_visiblity_is_required() { - - $attrs = array( - 'id' => 'field_id', - 'llms_visibility' => 'logged_in', - 'extra' => 'remains', - 'required' => true, - ); - $html = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) ); - $blocks = parse_blocks( $html ); - - $parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) ); - $expect = $attrs; - $expect['required'] = false; - $this->assertEquals( $expect, $parsed ); - - } - - /** - * Test cascade_visibility_attrs() for blocks with no innerBlocks. - * - * @since 5.0.0 - * - * @return void - */ - public function test_cascade_visibility_attrs_no_inner_blocks() { - - $blocks = parse_blocks( '<!-- wp:paragraph --><p>mock</p><!-- /wp:paragraph --><!-- wp:paragraph {"llms_visibility":"logged_out"} --><p>mock</p><!-- /wp:paragraph -->' ); - - // No changes to make. - $res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks ) ); - $this->assertEquals( $blocks, $res ); - - // Add the visibility setting. - $res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks, 'logged_in' ) ); - - // Changed. - $this->assertEquals( 'logged_in', $res[0]['attrs']['llms_visibility'] ); - - // Unchanged. - $this->assertEquals( 'logged_out', $res[1]['attrs']['llms_visibility'] ); - - } - - /** - * Test cascade_visibility_attrs() for blocks with innerBlocks. - * - * @since 5.0.0 - * - * @return void - */ - public function test_cascade_visibility_attrs_with_inner_blocks() { - - $blocks = parse_blocks( '<!-- wp:columns {"className":"has-2-columns"} --> - <div class="wp-block-columns has-2-columns"><!-- wp:column --> - <div class="wp-block-column"><!-- wp:paragraph --><p>mock</p><!-- /wp:paragraph --></div> - <!-- /wp:column --> - - <!-- wp:column --> - <div class="wp-block-column"><!-- wp:paragraph {"llms_visibility":"logged_out"} --><p>mock</p><!-- /wp:paragraph --></div> - <!-- /wp:column --></div> - <!-- /wp:columns -->' ); - - // No changes to make. - $res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks ) ); - $this->assertEquals( $blocks, $res ); - - // Add the visibility setting. - $res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks, 'logged_in' ) ); - - // Changed. - $this->assertEquals( 'logged_in', $res[0]['attrs']['llms_visibility'] ); - $this->assertEquals( 'logged_in', $res[0]['innerBlocks'][0]['attrs']['llms_visibility'] ); - $this->assertEquals( 'logged_in', $res[0]['innerBlocks'][0]['innerBlocks'][0]['attrs']['llms_visibility'] ); - - $this->assertEquals( 'logged_in', $res[0]['innerBlocks'][1]['attrs']['llms_visibility'] ); - - // Already had visibility so this one doesn't change. - $this->assertEquals( 'logged_out', $res[0]['innerBlocks'][1]['innerBlocks'][0]['attrs']['llms_visibility'] ); - - } - - /** - * Test creation for an invalid location. - * - * @since 5.0.0 - * - * @return void - */ - public function test_create_invalid_location() { - - $this->assertFalse( $this->forms->create( 'fake' ) ); - - } - - /** - * Test convert_settings_to_block_attrs() - * - * @since 5.0.0 - * - * @return void - */ - public function test_convert_settings_to_block_attrs() { - - $in = array( - 'id' => 'mock', - 'type' => 'text', - 'classes' => 'test', - 'attributes' => array(), - ); - - $out = array( - 'id' => 'mock', - 'field' => 'text', - 'className' => 'test', - 'html_attrs' => array(), - ); - - $this->assertEquals( $out, $this->forms->convert_settings_to_block_attrs( $in ) ); - - } - - /** - * Test convert_settings_format() for a block -> field transformation - * - * @since 5.0.0 - * - * @return void - */ - public function test_convert_settings_format_to_field() { - - $in = array( - 'id' => 'mock', - 'field' => 'text', - 'className' => 'test', - 'html_attrs' => array(), - ); - - $out = array( - 'id' => 'mock', - 'type' => 'text', - 'classes' => 'test', - 'attributes' => array(), - ); - - $this->assertEquals( $out, LLMS_Unit_Test_Util::call_method( $this->forms, 'convert_settings_format', array( $in, 'block' ) ) ); - - } - - /** - * Test creating/updating forms. - * - * @since 5.0.0 - * - * @return void - */ - public function test_create() { - - $locs = $this->forms->get_locations(); - $created = array(); - - // Create new forms. - foreach ( $locs as $loc => $data ) { - $id = $this->forms->create( $loc ); - $this->assertTrue( is_numeric( $id ) ); - $post = get_post( $id ); - $this->assertEquals( 'llms_form', $post->post_type ); - $this->assertEquals( $loc, get_post_meta( $post->ID, '_llms_form_location', true ) ); - - foreach ( $data['meta'] as $key => $val ) { - $this->assertEquals( $val, get_post_meta( $post->ID, $key, true ) ); - } - - $created[ $loc ] = $id; - - } - - // Locs already exist. - foreach ( array_keys( $locs ) as $loc ) { - $this->assertFalse( $this->forms->create( $loc ) ); - } - - // Locs already exist and we want to update them. - foreach ( array_keys( $locs ) as $loc ) { - $this->assertEquals( $created[ $loc ], $this->forms->create( $loc, true ), $loc ); - } - - } - - /** - * Test forms author on install - * - * @since 5.0.0 - * - * @return void - */ - public function test_forms_author_on_install() { - - // Clean user* tables. - global $wpdb; - $wpdb->query( "TRUNCATE TABLE $wpdb->users" ); - $wpdb->query( "TRUNCATE TABLE $wpdb->usermeta" ); - - // Create a subscriber. - $subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) ); - - $locs = $this->forms->get_locations(); - - // Install forms - $installed = $this->forms->install(); - - foreach ( $installed as $loc => $id ) { - // No admin users, expect 0. - $this->assertEquals( 0, get_post( $id )->post_author, $id ); - } - - // Delete forms. - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - // Create two admins. - $admins = $this->factory->user->create_many( 2, array( 'role' => 'administrator' ) ); - - // Install forms. - $installed = $this->forms->install(); - - foreach ( $installed as $loc => $id ) { - // Expect the first admin to be the forms author. - $this->assertEquals( $admins[0], get_post( $id )->post_author, $id ); - } - - // Delete forms. - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - // Log in as subscriber. - wp_set_current_user( $subscriber ); - - // Install forms. - $installed = $this->forms->install(); - - foreach ( $installed as $loc => $id ) { - // Expect the first admin to be the forms author. - $this->assertEquals( $admins[0], get_post( $id )->post_author, $id ); - } - - // Delete forms. - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - // Log in as first admin. - wp_set_current_user( $admins[0] ); - - // Install forms. - $installed = $this->forms->install(); - - foreach ( $installed as $loc => $id ) { - // Expect the first admin to be the forms author. - $this->assertEquals( $admins[0], get_post( $id )->post_author, $id ); - } - - // Delete forms. - $wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) ); - - // Log in as second admin. - wp_set_current_user( $admins[1] ); - - // Install forms. - $installed = $this->forms->install(); - - foreach ( $installed as $loc => $id ) { - // Expect the first admin to be the forms author. - $this->assertEquals( $admins[1], get_post( $id )->post_author, $id ); - } - - } - - /** - * Test the get_capability() method - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_capability() { - $this->assertEquals( 'manage_lifterlms', $this->forms->get_capability() ); - } - - - /** - * Test the get_fields_settings_from_blocks() method - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_fields_settings_from_blocks() { - - $this->forms->create( 'checkout', true ); - - $blocks = $this->forms->get_form_blocks( 'checkout' ); - - $fields = $this->forms->get_fields_settings_from_blocks( $blocks ); - - foreach ( $fields as $field ) { - $this->assertIsArray( $field ); - $this->assertTrue( ! empty( $field ) ); - } - - $expect = array( - 'email_address', - 'email_address_confirm', - 'password', - 'password_confirm', - 'llms-password-strength-meter', - 'first_name', - 'last_name', - 'llms_billing_address_1', - 'llms_billing_address_2', - 'llms_billing_city', - 'llms_billing_country', - 'llms_billing_state', - 'llms_billing_zip', - 'llms_phone', - ); - $this->assertEquals( $expect, wp_list_pluck( $fields, 'name' ) ); - - } - - /** - * Test get_free_enroll_form_fields() - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_free_enroll_form_fields() { - - $plan = $this->get_mock_plan(); - wp_set_current_user( $this->factory->user->create() ); - - $this->forms->create( 'checkout', true ); - - $fields = $this->forms->get_free_enroll_form_fields( $plan ); - - // Expected field list by name. - $expect = array( - 'first_name', - 'last_name', - 'llms_billing_address_1', - 'llms_billing_address_2', - 'llms_billing_city', - 'llms_billing_country', - 'llms_billing_state', - 'llms_billing_zip', - 'llms_phone', - 'free_checkout_redirect', - 'llms_plan_id', - ); - $this->assertEquals( $expect, wp_list_pluck( $fields, 'name' ) ); - - // Only hidden fields. - $this->assertEquals( array( 'hidden' ), array_unique( wp_list_pluck( $fields, 'type' ) ) ); - - } - - /** - * Can't retrieve blocks for an invalid location. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_blocks_invalid_location() { - - $this->assertFalse( $this->forms->get_form_blocks( 'fake' ) ); - - } - - /** - * Can't retrieve blocks for a location that hasn't been installed yet. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_blocks_not_installed() { - - foreach ( $this->get_form_locs() as $loc ) { - $this->assertFalse( $this->forms->get_form_blocks( $loc ) ); - } - - } - - /** - * Test get_form_blocks() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_blocks() { - - foreach ( $this->get_form_locs() as $loc ) { - - $this->forms->create( $loc ); - $blocks = $this->forms->get_form_blocks( $loc ); - - foreach ( $blocks as $block ) { - $this->assertIsABlock( $block ); - } - - } - - - } - - /** - * Can't retrieve fields for an invalid location. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_fields_invalid_loc() { - $this->assertFalse( $this->forms->get_form_fields( 'fake' ) ); - } - - /** - * Can't retrieve fields for a location that hasn't been installed yet. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_fields_not_installed() { - foreach ( $this->get_form_locs() as $loc ) { - $this->assertFalse( $this->forms->get_form_fields( $loc ) ); - } - } - - /** - * Test get_form_fields() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_fields() { - - foreach ( $this->get_form_locs() as $loc ) { - $this->forms->create( $loc ); - $fields = $this->forms->get_form_fields( $loc ); - - foreach ( $fields as $field ) { - $this->assertIsAField( $field ); - } - } - - } - - /** - * Can't get form html for an invalid form. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_html_invalid() { - - $this->assertEquals( '', $this->forms->get_form_html( 'fake' ) ); - - } - - /** - * Can't get form html for a form that hasn't been installed. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_html_not_installed() { - - foreach ( $this->get_form_locs() as $loc ) { - $this->assertEquals( '', $this->forms->get_form_html( $loc ) ); - } - - } - - /** - * Test get_form_html() method. - * - * @since 5.0.0 - * - * @todo this test can assert a lot more and should. - * - * @return void - */ - public function test_get_form_html() { - - foreach ( $this->get_form_locs() as $loc ) { - $this->forms->create( $loc ); - $html = $this->forms->get_form_html( $loc ); - - $this->assertStringContains( '<div class="llms-form-field type-email', $html ); - } - - } - - /** - * Can't retrieve a post for an invalid location. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_post_invalid() { - - $this->assertFalse( $this->forms->get_form_post( 'fake' ) ); - - } - - /** - * Test get_form_post() for forms when they're not installed. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_post_not_installed() { - - foreach ( $this->get_form_locs() as $loc ) { - $this->assertFalse( $this->forms->get_form_post( $loc ) ); - } - - } - - /** - * Test get_form_post() - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_form_post() { - - foreach ( $this->get_form_locs() as $loc ) { - $id = $this->forms->create( $loc ); - $this->assertEquals( get_post( $id ), $this->forms->get_form_post( $loc ) ); - } - - } - - /** - * Test get_locations() method. - * - * @since 5.0.0 - * - * @see {Reference} - * @link {URL} - * - * @return void - */ - public function test_get_locations() { - - $locs = $this->forms->get_locations(); - foreach ( $this->get_form_locs() as $loc ) { - $this->assertArrayHasKey( $loc, $locs ); - $this->assertArrayHasKey( 'name', $locs[ $loc ] ); - $this->assertArrayHasKey( 'description', $locs[ $loc ] ); - $this->assertArrayHasKey( 'title', $locs[ $loc ] ); - $this->assertArrayHasKey( 'meta', $locs[ $loc ] ); - } - - } - - /** - * Test the get_post_type() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_post_type() { - $this->assertEquals( 'llms_form', $this->forms->get_post_type() ); - } - - /** - * test the install() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_install() { - - $installed = $this->forms->install(); - $this->assertEquals( 3, count( $installed ) ); - - foreach( $installed as $id ) { - $post = get_post( $id ); - $this->assertTrue( is_a( $post, 'WP_Post' ) ); - $this->assertEquals( 'llms_form', $post->post_type ); - } - - // Already installed. - $installed = $this->forms->install(); - foreach ( $installed as $id ) { - $this->assertFalse( $id ); - } - - } - - /** - * Test is_block_visible() when no visibility settings exist on the block. - * - * @since 5.0.0 - * - * @return void - */ - public function test_is_block_visible_no_visibility() { - - $blocks = parse_blocks( '<!-- wp:paragraph --><p>Fake paragraph content</p><!-- /wp:paragraph -->' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) ); - - } - - /** - * Test is_block_visible() when there are visibility settings which would affect the visibility of the block. - * - * @since 5.0.0 - * - * @return void - */ - public function test_is_block_visible_with_visibility() { - - // Logged out users only. - $blocks = parse_blocks( '<!-- wp:paragraph {"llms_visibility":"logged_out"} --><p>Fake paragraph content</p><!-- /wp:paragraph -->' ); - - // No user, show the block. - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) ); - - // Has a user, don't show. - wp_set_current_user( $this->factory->student->create() ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) ); - - } - - /** - * Test is_block_visible_in_list() - * - * This additionally covers conditions in get_block_path(). - * - * @since 5.1.0 - * - * @return void - */ - public function test_is_block_visible_in_list() { - - $hidden_json = '{"llms_visibility":"logged_in","llms_visibility_in":"any_course"}'; - - $visible = '<!-- wp:paragraph -->\n<p>Test</p>\n<!-- /wp:paragraph -->'; - $hidden = sprintf( '<!-- wp:paragraph %s -->\n<p>Test</p>\n<!-- /wp:paragraph -->', $hidden_json ); - - /** - * List of tests to run - * - * @param array[] { - * @type string $0 Test description / message. Passed to the assertion for debugging failed tests. - * @type string $1 Block markup for the block being tested. - * @type string $2 List of blocks for use as second parameter. The HTML from $1 must be found in this list! - * @type bool $3 The expected result of `is_block_visible_in_list()`. - * } - */ - $tests = array( - - array( - 'Block not found in the list', - $visible, - $hidden, - false, - ), - - array( - 'Empty list falls back to `is_block_visible()`: is visible', - $visible, - '', - true, - ), - - array( - 'Empty list falls back to `is_block_visible()`: not visible', - $hidden, - '', - false, - ), - - array( - 'Flat list: is visible', - $visible, - $hidden . $visible, - true, - ), - - array( - 'Flat list: not visible', - $hidden, - $visible . $hidden, - false, - ), - - array( - 'Visible in a group', - $visible, - sprintf( '<!-- wp:group -->\n<div class="wp-block-group">%s</div>\n<!-- /wp:group -->', $visible ), - true, - ), - - array( - 'Hidden in a group', - $hidden, - sprintf( '<!-- wp:group -->\n<div class="wp-block-group">%s</div>\n<!-- /wp:group -->', $hidden ), - false, - ), - - array( - 'Visible in a hidden group', - $visible, - sprintf( '<!-- wp:group %1$s -->\n<div class="wp-block-group">%2$s</div>\n<!-- /wp:group -->', $hidden_json, $visible ), - false, - ), - - array( - 'Hidden in a hidden group', - $hidden, - sprintf( '<!-- wp:group %1$s -->\n<div class="wp-block-group">%2$s</div>\n<!-- /wp:group -->', $hidden_json, $hidden ), - false, - ), - - array( - 'Multiple parents: visible -> visible -> visible', - $visible, - sprintf( '<!-- 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 ), - true, - ), - - array( - 'Multiple parents: hidden -> hidden -> hidden', - $hidden, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: visible -> visible -> hidden', - $hidden, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: visible -> hidden -> hidden', - $hidden, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: hidden -> hidden -> visible', - $visible, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: hidden -> visible -> visible', - $visible, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: hidden -> visible -> hidden', - $hidden, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Multiple parents: visible -> hidden -> visible', - $visible, - sprintf( '<!-- 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 ), - false, - ), - - array( - 'Break Stuff', - $visible, - sprintf( '<!-- 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 ), - false, - ), - - ); - - foreach ( $tests as $data ) { - - $msg = $data[0]; - $block = parse_blocks( $data[1] )[0]; - $list = parse_blocks( $data[2] ); - $expect = $data[3]; - - $this->assertEquals( $expect, $this->forms->is_block_visible_in_list( $block, $list ), $msg ); - - } - - - } - - /** - * Test get_block_tree() - * - * @since 5.1.1 - * - * @return void - */ - public function test_get_block_tree() { - - $test_block_json = '<!-- wp:paragraph -->\n<p>Test</p>\n<!-- /wp:paragraph -->'; - $group_block_json = '<!-- wp:group -->\n<div class="wp-block-group">%1$s</div>\n<!-- /wp:group -->'; - - $test_block_as_parent_json = sprintf( $group_block_json, $test_block_json ); - - /** - * List of tests to run - * - * @param array[] { - * @type string $0 Test description / message. Passed to the assertion for debugging failed tests. - * @type string $1 Block markup for the block being tested. - * @type string $2 List of blocks for use as second parameter. The HTML from $1 must be found in this list! - * @type bool $3 The expected result of `get_block_tree()`. - * } - */ - $tests = array( - - array( - 'Block in a tree with two levels, with the leaf\'s parent branch having one sibling', - $test_block_json, - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_json - ) . - sprintf( - $group_block_json, - 'Suppressed' - ) - ), - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_json - ) - ) - ), - - array( - 'Block in a tree with two levels, with the leaf\'s gran parent\'s branch having one sibling', - $test_block_json, - sprintf( - $group_block_json, - sprintf( - $group_block_json, - 'Suppressed' - ) . - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_json - ) - ) - ), - sprintf( - $group_block_json, - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_json - ) - ) - ), - ), - - array( - 'No block found', - $test_block_json, - sprintf( - $group_block_json, - sprintf( - $group_block_json, - 'Something' - ) . - sprintf( - $group_block_json, - sprintf( - $group_block_json, - 'Something Else' - ) - ) - ), - '' - ), - - array( - 'Block as first of the list', - $test_block_json, - $test_block_json, - $test_block_json - ), - - array( - 'Block\'s children preserved', - $test_block_as_parent_json, - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_as_parent_json - ) . - sprintf( - $group_block_json, - 'Suppressed' - ) - ), - sprintf( - $group_block_json, - sprintf( - $group_block_json, - $test_block_as_parent_json - ) - ), - ), - - ); - - foreach ( $tests as $data ) { - - $msg = $data[0]; - $block = parse_blocks( $data[1] )[0]; - $list = parse_blocks( $data[2] ); - $expect = parse_blocks( $data[3] ); - - $this->assertEquals( - $expect, - LLMS_Unit_Test_Util::call_method( $this->forms, 'get_block_tree', array( $block, $list ), $msg ) - ); - - } - - } - - /** - * Test is_location_valid() - * - * @since 5.0.0 - * - * @return void - */ - public function test_is_location_valid() { - - foreach ( array_keys( $this->forms->get_locations() ) as $loc ) { - $this->assertTrue( $this->forms->is_location_valid( $loc ) ); - } - - $this->assertFalse( $this->forms->is_location_valid( 'fake' ) ); - - } - - /** - * Test load_reusable_blocks() default successful behavior. - * - * @since 5.0.0 - * - * @return void - */ - public function test_load_reusable_blocks() { - - $blocks = array( - LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( 'email' ) ), - LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( 'address' ) ), - ); - - $load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) ); - - // Make sure the loaded blocks match the following snapshot when serialized. - $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 -->'; - $this->assertEquals( parse_blocks( $expected ), parse_blocks( serialize_blocks( $load ) ) ); - - } - - /** - * Test load_reusable_blocks(): a non-existent block is passed in - * - * @since 5.0.0 - * - * @return void - */ - public function test_load_reusable_blocks_fake() { - - $blocks = array( - array( - 'blockName' => 'core/block', - 'attrs' => array( 'ref' => $this->factory->post->create() + 1 ), - 'innerContent' => array(), - ), - ); - - $load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) ); - $this->assertEquals( array(), $load ); - - } - - /** - * Test load_reusable_blocks(): when the reusable block is not published. - * - * @since 5.0.0 - * - * @return void - */ - public function test_load_reusable_blocks_draft() { - - $blocks = array( - array( - 'blockName' => 'core/block', - 'attrs' => array( 'ref' => $this->factory->post->create( array( 'post_status' => 'draft' ) ) ), - 'innerContent' => array(), - ), - ); - - $load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) ); - $this->assertEquals( array(), $load ); - - } - - /** - * Test maybe_load_preview() when no post is found - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_load_preview_no_post() { - $this->assertFalse( $this->forms->maybe_load_preview( false ) ); - } - - /** - * Test maybe_load_preview() when not previewing - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_load_preview_not_preview() { - $post = $this->factory->post->create_and_get(); - $this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) ); - } - - /** - * Test maybe_load_preview() when current user can't preview - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_load_preview_user_cant_preview() { - global $wp_query; - $post = $this->factory->post->create_and_get(); - $save = (array) $post; - $save['post_ID'] = $save['ID']; - $save['post_content'] = 'autosave content'; - wp_create_post_autosave( $save ); - $wp_query->is_preview(); - $this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) ); - } - - /** - * Test maybe_load_preview() when there is a preview - * - * @since 5.0.0 - * - * @return void - */ - public function test_maybe_load_preview_user_can_preview() { - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - global $wp_query; - $post = $this->factory->post->create_and_get(); - $save = (array) $post; - $save['post_ID'] = $save['ID']; - $save['post_content'] = 'autosave content'; - wp_create_post_autosave( $save ); - $wp_query->is_preview(); - $this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) ); - } - - /** - * Test block field render function for non-field blocks. - * - * @since 5.0.0 - * - * @return void - */ - public function test_render_field_block_non_field_block() { - - $html = '<p>Fake paragraph content</p>'; - $blocks = parse_blocks( '<!-- wp:paragraph -->' . $html . '<!-- /wp:paragraph -->' ); - $this->assertEquals( $html, $this->forms->render_field_block( $html, $blocks[0] ) ); - - } - - /** - * Test rendering a field block as a field. - * - * @since 5.0.0 - * - * @return void - */ - public function test_render_field_block() { - - $atts = array( - 'id' => 'field_id', - ); - - $blocks = parse_blocks( '<!-- wp:llms/form-field-text {"id":"field_id"} /-->' ); - - $this->assertEquals( llms_form_field( $atts, false ), $this->forms->render_field_block( '', $blocks[0] ) ); - - } - - /** - * Test rendering a field block which contains fields in the inner blocks - * - * @since 5.0.0 - * - * @return void - */ - public function test_render_field_block_with_inner() { - - $blocks = parse_blocks( '<!-- wp:llms/form-field-confirm-group --> -<!-- wp:llms/form-field-user-email {"id":"one"} /--> - -<!-- wp:llms/form-field-text {"id":"two"} /--> -<!-- /wp:llms/form-field-confirm-group -->' ); - - ob_start(); - llms_form_field( array( 'id' => 'one' ) ); - echo "\n"; - llms_form_field( array( 'id' => 'two' ) ); - $expected = ob_get_clean(); - - $this->assertEquals( $expected, $this->forms->render_field_block( '', $blocks[0] ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-courses.php b/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-courses.php deleted file mode 100644 index 36169138bc..0000000000 --- a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-courses.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * Course template function tests - * - * @group functions - * @group template_functions_courses - * @group template_functions - * - * @since 4.11.0 - */ -class LLMS_Test_Functions_Templates_Courses extends LLMS_Unit_Test_Case { - - /** - * Test lifterlms_template_course_author() - * - * @since 4.11.0 - * - * @return void - */ - public function test_lifterlms_template_course_author() { - - $user = $this->factory->user->create_and_get( array( - 'first_name' => 'Jimothy', - 'last_name' => 'Halpert', - 'description' => 'Paper salesman at Dunder Mifflin Scranton.' - ) ); - $user2 = $this->factory->user->create_and_get( array( - 'first_name' => 'Dwight', - 'last_name' => 'Schrute', - 'description' => 'Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.' - ) ); - - global $post; - $post = $this->factory->post->create_and_get( array( - 'post_type' => 'course', - 'post_author' => $user->ID, - ) ); - $course = llms_get_post( $post ); - - // One user (default post author). - $course->instructors()->set_instructors( array() ); - - $template = $this->get_output( 'lifterlms_template_course_author' ); - - $this->assertStringContains( 'Course Instructor', $template ); - $this->assertStringContains( '<div class="llms-col-1">', $template ); - $this->assertStringContains( '<span class="llms-author-info name">Jimothy Halpert</span>', $template ); - $this->assertStringContains( '<span class="llms-author-info label">Author</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Paper salesman at Dunder Mifflin Scranton.</p>', $template ); - - // Two Instructors. - $course->instructors()->set_instructors( array( array( 'id' => $user->ID ), array( 'id' => $user2->ID ) ) ); - - $template = $this->get_output( 'lifterlms_template_course_author' ); - - $this->assertStringContains( 'Course Instructors', $template ); - $this->assertStringContains( '<div class="llms-col-2">', $template ); - - $this->assertStringContains( '<span class="llms-author-info name">Jimothy Halpert</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Paper salesman at Dunder Mifflin Scranton.</p>', $template ); - - $this->assertStringContains( '<span class="llms-author-info name">Dwight Schrute</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.</p>', $template ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-memberships.php b/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-memberships.php deleted file mode 100644 index 6f3414c724..0000000000 --- a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-memberships.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * Membership template function tests - * - * @group functions - * @group template_functions_memberships - * @group template_functions - * - * @since 4.11.0 - */ -class LLMS_Test_Functions_Templates_Memberships extends LLMS_Unit_Test_Case { - - /** - * Test llms_template_membership_instructors() - * - * @since 4.11.0 - * - * @return void - */ - public function test_llms_template_membership_auinstructors() { - - $user = $this->factory->user->create_and_get( array( - 'first_name' => 'Jimothy', - 'last_name' => 'Halpert', - 'description' => 'Paper salesman at Dunder Mifflin Scranton.' - ) ); - $user2 = $this->factory->user->create_and_get( array( - 'first_name' => 'Dwight', - 'last_name' => 'Schrute', - 'description' => 'Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.' - ) ); - - global $post; - $post = $this->factory->post->create_and_get( array( - 'post_type' => 'llms_membership', - 'post_author' => $user->ID, - ) ); - $membership = llms_get_post( $post ); - - // One user (default post author). - $membership->instructors()->set_instructors( array() ); - - $template = $this->get_output( 'llms_template_membership_instructors' ); - - $this->assertStringContains( 'Membership Instructor', $template ); - $this->assertStringContains( '<div class="llms-col-1">', $template ); - $this->assertStringContains( '<span class="llms-author-info name">Jimothy Halpert</span>', $template ); - $this->assertStringContains( '<span class="llms-author-info label">Author</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Paper salesman at Dunder Mifflin Scranton.</p>', $template ); - - // Two Instructors. - $membership->instructors()->set_instructors( array( array( 'id' => $user->ID ), array( 'id' => $user2->ID ) ) ); - - $template = $this->get_output( 'llms_template_membership_instructors' ); - - $this->assertStringContains( 'Membership Instructors', $template ); - $this->assertStringContains( '<div class="llms-col-2">', $template ); - - $this->assertStringContains( '<span class="llms-author-info name">Jimothy Halpert</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Paper salesman at Dunder Mifflin Scranton.</p>', $template ); - - $this->assertStringContains( '<span class="llms-author-info name">Dwight Schrute</span>', $template ); - $this->assertStringContains( '<p class="llms-author-info bio">Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.</p>', $template ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-pricing-table.php b/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-pricing-table.php deleted file mode 100644 index 6b851ed71e..0000000000 --- a/tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-pricing-table.php +++ /dev/null @@ -1,273 +0,0 @@ -<?php -/** - * Tests for LifterLMS User Postmeta functions - * @group functions - * @group template_functions - * @group pricing_tables - * @since 3.23.0 - * @version 3.23.0 - */ -class LLMS_Test_Functions_Templates_Pricing_Tables extends LLMS_UnitTestCase { - - /** - * Retrieve output buffer for a given template function and access plan - * @param string $func template function name - * @param array $plan_args plan arguments, passed to $this->get_mock_plan() - * @param obj $plan optionally pass a plan (ignores $plan_args) - * @return array - * @since 3.23.0 - * @version 3.23.0 - */ - private function get_ob( $func, $plan_args = array(), $plan = null ) { - - if ( is_null( $plan ) ) { - $plan = call_user_func_array( array( $this, 'get_mock_plan' ), $plan_args ); - } - - ob_start(); - call_user_func( $func, $plan ); - return array( - 'plan' => $plan, - 'html' => trim( ob_get_clean() ), - ); - - } - - /** - * test the llms_get_access_plan_classes method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_get_access_plan_classes() { - - $expect = 'llms-access-plan llms-access-plan-%d'; - - $plan = $this->get_mock_plan(); - $this->assertEquals( sprintf( $expect, $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) ); - - // on sale - $plan = $this->get_mock_plan( 1, 1, 'liftetime', true ); - $this->assertEquals( sprintf( $expect . ' on-sale', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) ); - - // featured - $plan = $this->get_mock_plan(); - $plan->set_visibility( 'featured' ); - $this->assertEquals( sprintf( $expect . ' featured', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) ); - - // featured & on sale - $plan = $this->get_mock_plan( 1, 1, 'liftetime', true ); - $plan->set_visibility( 'featured' ); - $this->assertEquals( sprintf( $expect . ' featured on-sale', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) ); - - } - - /** - * test the llms_template_access_plan method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan() { - - $ob = $this->get_ob( 'llms_template_access_plan' ); - - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan' ) ); - $this->assertTrue( strlen( $ob['html'] ) - 6 === strrpos( $ob['html'], '</div>' ) ); - $this->assertTrue( false !== strpos( $ob['html'], sprintf( 'id="llms-access-plan-%d"', $ob['plan']->get( 'id' ) ) ) ); - $this->assertEquals( 1, did_action( 'llms_before_access_plan' ) ); - $this->assertEquals( 1, did_action( 'llms_acces_plan_content' ) ); - $this->assertEquals( 1, did_action( 'llms_acces_plan_footer' ) ); - - } - - /** - * test the llms_template_access_plan_button method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_button() { - - LLMS_Install::create_pages(); - $ob = $this->get_ob( 'llms_template_access_plan_button', array( 0 ) ); - - // purchase button link - $this->assertTrue( false !== strpos( $ob['html'], '<a class="llms-button-action button"' ) ); - $this->assertTrue( false !== strpos( $ob['html'], sprintf( 'href="%s"', $ob['plan']->get_checkout_url() ) ) ); - - // check free enroll form - $student = $this->get_mock_student(); - wp_set_current_user( $student->get_id() ); - $ob['plan']->set( 'is_free', 'yes' ); - $ob = $this->get_ob( 'llms_template_access_plan_button', array(), $ob['plan'] ); - $this->assertTrue( 0 === strpos( $ob['html'], '<form' ) ); - - } - - /** - * test the llms_template_access_plan_description method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_description() { - - $plan = $this->get_mock_plan(); - $plan->set( 'content', '<p>mock description</p>' ); - - $ob = $this->get_ob( 'llms_template_access_plan_description', array(), $plan ); - - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan-description">' ) ); - $this->assertTrue( false !== strpos( $ob['html'], '<p>mock description</p>' ) ); - - } - - /** - * test the llms_template_access_plan_feature method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_feature() { - - $ob = $this->get_ob( 'llms_template_access_plan_feature', array() ); - - // not featured - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan-featured">' ) ); - $this->assertTrue( false === strpos( $ob['html'], 'FEATURED' ) ); - - // featured - $ob['plan']->set_visibility( 'featured' ); - $ob = $this->get_ob( 'llms_template_access_plan_feature', array(), $ob['plan'] ); - $this->assertTrue( false !== strpos( $ob['html'], 'FEATURED' ) ); - - } - - /** - * test the llms_template_access_plan_pricing method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_pricing() { - - // single, not on sale, no expiration, no recurring - $ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0 ) ); - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan-pricing regular">' ) ); - $this->assertTrue( false !== strpos( $ob['html'], llms_price( 1 ) ) ); - $this->assertTrue( false === strpos( $ob['html'], 'SALE' ) ); - $this->assertTrue( false === strpos( $ob['html'], 'class="llms-access-plan-schedule"' ) ); - $this->assertTrue( false === strpos( $ob['html'], 'class="llms-access-plan-expiration"' ) ); - - // on sale - $ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0, 'liftetime', true ) ); - $this->assertTrue( false !== strpos( $ob['html'], 'SALE' ) ); - - // expires - $ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0, 'limited-date' ) ); - $this->assertTrue( false !== strpos( $ob['html'], 'class="llms-access-plan-expiration"' ) ); - - // recurring - $ob = $this->get_ob( 'llms_template_access_plan_pricing' ); - $this->assertTrue( false !== strpos( $ob['html'], 'class="llms-access-plan-schedule"' ) ); - - } - - /** - * test the llms_template_access_plan_restrictions method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_restrictions() { - - $ob = $this->get_ob( 'llms_template_access_plan_restrictions' ); - $this->assertEmpty( $ob['html'] ); - - // has restriction - $mid = $this->factory->post->create( array( - 'post_type' => 'llms_membership', - ) ); - $ob['plan']->set( 'availability', 'members' ); - $ob['plan']->set( 'availability_restrictions', array( $mid ) ); - - $ob = $this->get_ob( 'llms_template_access_plan_restrictions', array(), $ob['plan'] ); - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan-restrictions">' ) ); - $this->assertTrue( false !== strpos( $ob['html'], get_the_title( $mid ) ) ); - - } - - /** - * test the llms_template_access_plan_title method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_title() { - - $ob = $this->get_ob( 'llms_template_access_plan_title' ); - $this->assertEquals( sprintf( '<h4 class="llms-access-plan-title">%s</h4>', $ob['plan']->get( 'title' ) ), $ob['html'] ); - - } - - /** - * test the llms_template_access_plan_trial method - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_llms_template_access_plan_trial() { - - // no trial - $ob = $this->get_ob( 'llms_template_access_plan_trial' ); - $this->assertTrue( 0 === strpos( $ob['html'], '<div class="llms-access-plan-pricing trial">' ) ); - $this->assertTrue( false === strpos( $ob['html'], 'TRIAL' ) ); - - // has trial - $ob = $this->get_ob( 'llms_template_access_plan_trial', array( 1, 1, 'lifetime', false, true ) ); - $this->assertTrue( false !== strpos( $ob['html'], 'TRIAL' ) ); - - } - - /** - * test test_lifterlms_template_pricing_table method - * @todo add tests to test logic in template - * @return void - * @since 3.23.0 - * @version 3.23.0 - */ - public function test_lifterlms_template_pricing_table() { - - $plan = $this->get_mock_plan(); - $course = $plan->get_product(); - - $plan = $this->get_mock_plan( 15 ); - $plan->set( 'product_id', $course->get( 'id' ) ); - - $plan = $this->get_mock_plan( 1 ); - $plan->set( 'product_id', $course->get( 'id' ) ); - - $manual = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $manual->get_option_name( 'enabled' ), 'no' ); - - // no gateways available - ob_start(); - lifterlms_template_pricing_table( $course->get( 'id' ) ); - $html = trim( ob_get_clean() ); - - $this->assertEmpty( $html ); - - // gateways available - update_option( $manual->get_option_name( 'enabled' ), 'yes' ); - - ob_start(); - lifterlms_template_pricing_table( $course->get( 'id' ) ); - $html = trim( ob_get_clean() ); - - $this->assertTrue( 0 === strpos( $html, '<section class="llms-access-plans cols-3">' ) ); - $this->assertEquals( 3, did_action( 'llms_access_plan' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-access-plans.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-access-plans.php deleted file mode 100644 index 97058bb096..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-access-plans.php +++ /dev/null @@ -1,483 +0,0 @@ -<?php -/** - * Test Order Functions - * - * @package LifterLMS/Tests - * - * @group LLMS_Access_Plan - * - * @since 3.29.0 - * @since 3.32.0 Add delta to date assertions for `test_llms_insert_access_plan_update()`. - * @since 3.34.0 Add gmt date to list of date fields that should be assersted with a delta. - */ -class LLMS_Test_Functions_Access_Plans extends LLMS_UnitTestCase { - - /** - * Test the llms_get_access_plan_period_options() method - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_get_access_plan_period_options() { - - $options = llms_get_access_plan_period_options(); - $this->assertEquals( array( 'year', 'month', 'week', 'day' ), array_keys( $options ) ); - - } - - /** - * Test the llms_get_access_plan_visibility_options() method - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_get_access_plan_visibility_options() { - - $options = llms_get_access_plan_visibility_options(); - $this->assertEquals( array( 'visible', 'hidden', 'featured' ), array_keys( $options ) ); - - } - - /** - * Test default props for llms_insert_access_plan() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_default() { - - $props = array(); - $props['product_id'] = $this->factory->course->create( array( 'sections' => 0 ) ); - - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Default properties. - $this->assertEquals( 0, $plan->get( 'price' ) ); - $this->assertEquals( 'yes', $plan->get( 'is_free' ) ); - $this->assertEquals( 'no', $plan->get( 'on_sale' ) ); - $this->assertEquals( 0, $plan->get( 'frequency' ) ); - $this->assertEquals( 'Access Plan', $plan->get( 'title' ) ); - - // No possible trial. - $this->assertEquals( 'no', $plan->get( 'trial_offer' ) ); - $this->assertEmpty( $plan->get( 'trial_price' ) ); - $this->assertEmpty( $plan->get( 'trial_length' ) ); - $this->assertEmpty( $plan->get( 'trial_period' ) ); - - // Expiration. - $this->assertEquals( 'lifetime', $plan->get( 'access_expiration' ) ); - - } - - /** - * Test the default parameters that will be automatically "fixed" or overridden for the llms_insert_access_plan() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_free_default_overrides() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'price' => 0, - 'is_free' => 'no', - 'frequency' => 0, - 'on_sale' => 'yes', - 'trial_offer' => 'yes', - ); - - $plan = llms_insert_access_plan( $props ); - - // Success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - $this->assertEquals( 0, $plan->get( 'price' ) ); - $this->assertEquals( 'yes', $plan->get( 'is_free' ) ); - $this->assertEquals( 'no', $plan->get( 'on_sale' ) ); - $this->assertEquals( 'no', $plan->get( 'trial_offer' ) ); - $this->assertEquals( 0, $plan->get( 'frequency' ) ); - - } - - /** - * Test recurring payment props for llms_insert_access_plan() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_payment_recurring() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'price' => 5, - 'frequency' => 1, - 'length' => 1, - 'period' => 'week', - ); - - $plan = llms_insert_access_plan( $props ); - - // Success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Props. - $this->assertEquals( 5, $plan->get( 'price' ) ); - $this->assertEquals( 1, $plan->get( 'frequency' ) ); - $this->assertEquals( 1, $plan->get( 'length' ) ); - $this->assertEquals( 'week', $plan->get( 'period' ) ); - - } - - /** - * Test one-time payment props for llms_insert_access_plan() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_payment_single() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'price' => 5, - 'frequency' => 0, - 'length' => 1, - 'period' => 'week', - ); - - $plan = llms_insert_access_plan( $props ); - - // Success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Props. - $this->assertEquals( 5, $plan->get( 'price' ) ); - $this->assertEquals( 0, $plan->get( 'frequency' ) ); - $this->assertEmpty( $plan->get( 'length' ) ); - $this->assertEmpty( $plan->get( 'period' ) ); - - } - - /** - * Test sale-related props on the llms_insert_access_plan() function - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_props_sale() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'on_sale' => 'yes', - 'price' => 50, - ); - - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Default sale. - $this->assertEquals( 'yes', $plan->get( 'on_sale' ) ); - $this->assertEquals( 0, $plan->get( 'sale_price' ) ); - - // Other props. - $props['sale_price'] = 25; - $props['on_sale'] = 'yes'; - $props['sale_end'] = '2019-05-05'; - $props['sale_start'] = '2019-05-05'; - - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Test props. - $this->assertEquals( $props['sale_price'], $plan->get( 'sale_price' ) ); - $this->assertEquals( $props['on_sale'], $plan->get( 'on_sale' ) ); - $this->assertEquals( $props['sale_end'], $plan->get( 'sale_end' ) ); - $this->assertEquals( $props['sale_start'], $plan->get( 'sale_start' ) ); - - } - - /** - * Test expiration-related props on the llms_insert_access_plan() function - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_props_expiration() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'access_expiration' => 'lifetime', - ); - - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Props. - $this->assertEquals( 'lifetime', $plan->get( 'access_expiration' ) ); - $this->assertEmpty( $plan->get( 'access_expires' ) ); - $this->assertEmpty( $plan->get( 'access_length' ) ); - $this->assertEmpty( $plan->get( 'access_period' ) ); - - // Limited Date. - $props['access_expiration'] = 'limited-date'; - $props['access_expires'] = '2019-02-14'; // naw.... so much <3. - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Props. - $this->assertEquals( 'limited-date', $plan->get( 'access_expiration' ) ); - $this->assertEquals( $props['access_expires'], $plan->get( 'access_expires' ) ); - $this->assertEmpty( $plan->get( 'access_length' ) ); - $this->assertEmpty( $plan->get( 'access_period' ) ); - - // Limited Period. - $props['access_expiration'] = 'limited-period'; - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Props. - $this->assertEquals( 'limited-period', $plan->get( 'access_expiration' ) ); - $this->assertEquals( 1, $plan->get( 'access_length' ) ); - $this->assertEquals( 'year', $plan->get( 'access_period' ) ); - $this->assertEmpty( $plan->get( 'access_expires' ) ); - - } - - /** - * Test trial-related props on the llms_insert_access_plan() function - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_props_trial() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'trial_offer' => 'yes', - 'trial_length' => 1, - 'trial_period' => 'year', - ); - - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // No trial on a free plan. - $this->assertEquals( 'no', $plan->get( 'trial_offer' ) ) ; - - $props['price'] = 1; - $plan = llms_insert_access_plan( $props ); - - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // No trial for one-time payments. - $this->assertEquals( 'no', $plan->get( 'trial_offer' ) ) ; - - $props['frequency'] = 1; - $plan = llms_insert_access_plan( $props ); - // Creation success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - $this->assertEquals( 'yes', $plan->get( 'trial_offer' ) ) ; - $this->assertEquals( 0, $plan->get( 'trial_price' ) ); - $this->assertEquals( 1, $plan->get( 'trial_length' ) ); - $this->assertEquals( 'year', $plan->get( 'trial_period' ) ); - - } - - - /** - * Test updating existing llms_insert_access_plan() function. - * - * @since 3.29.0 - * @since 3.32.0 Add delta to date assertions. - * @since 3.34.0 Add gmt date to list of date fields that should be assersted with a delta. - * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter to `assertEquals()`. - * - * @return void - */ - public function test_llms_insert_access_plan_update() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'price' => 1, - ); - - // Create. - $plan = llms_insert_access_plan( $props ); - - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - $this->assertEquals( 1, did_action( 'llms_access_plan_after_create' ) ); - $this->assertEquals( 1, $plan->get( 'price' ) ); - - // Update with a a fake ID. - $props['id'] = 'fake'; - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'invalid-plan', llms_insert_access_plan( $props ) ); - - // Update with a valid post ID (but not the access plan post type). - $props['id'] = $this->factory->post->create(); - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'invalid-plan', llms_insert_access_plan( $props ) ); - - // plan before props. - $plan_before = $plan->toArray(); - - // Real plan. - $props['id'] = $plan->get( 'id' ); - $props['price'] = 2; - $plan = llms_insert_access_plan( $props ); - - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - $this->assertEquals( 1, did_action( 'llms_access_plan_after_update' ) ); - $this->assertEquals( 2, $plan->get( 'price' ) ); - - // Price is the only property that should have changed. - foreach ( $plan->toArray() as $key => $val ) { - if ( 'price' === $key ) { - $this->assertFalse( $plan_before[ $key ] === $val ); - } elseif ( in_array( $key, array( 'date', 'date_gmt', 'modified', 'modified_gmt' ), true ) ) { - $this->assertEqualsWithDelta( strtotime( $plan_before[ $key ] ), strtotime( $val ), 5, $key ); - } else { - $this->assertEquals( $plan_before[ $key ], $val, $key ); - } - } - - } - - /** - * Test period field validators for the llms_insert_access_plan_validation() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_validation_period() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - 'price' => 1, - 'frequency' => 1, - 'trial_offer' => 'yes', - 'access_expiration' => 'limited-period', - ); - - foreach ( array( 'period', 'access_period', 'trial_period' ) as $period_prop ) { - - foreach ( array( 'year', 'month', 'week', 'day' ) as $period ) { - - $props[ $period_prop ] = $period; - - $plan = llms_insert_access_plan( $props ); - - // Success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Getter matches set value. - $this->assertEquals( $period, $plan->get( $period_prop ) ); - - } - - // Doesn't work with an invalid visibility. - $props[ $period_prop ] = 'fake'; - $plan = llms_insert_access_plan( $props ); - $this->assertIsWPError( $plan ); - $this->assertWPErrorCodeEquals( 'invalid-' . $period_prop, $plan ); - unset( $props[ $period_prop ] ); - - } - - } - - /** - * Test product related conditions for llms_insert_access_plan() function. - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_validation_product() { - - $props = array(); - - // Missing Product ID. - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) ); - - // Set but empty. - $props['product_id'] = ''; - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) ); - - // Not an ID. - $props['product_id'] = 'fake'; - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) ); - - // Real Product. - $props['product_id'] = $this->factory->course->create( array( 'sections' => 0 ) ); - $this->assertTrue( is_a( llms_insert_access_plan( $props ), 'LLMS_Access_Plan' ) ); - $this->assertEquals( 1, did_action( 'llms_access_plan_after_create' ) ); - - } - - /** - * Test plan visibility validation for the llms_insert_access_plan() function - * - * @since 3.29.0 - * - * @return void - */ - public function test_llms_insert_access_plan_validation_visibility() { - - $props = array( - 'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ), - ); - - // Invalid visibility. - $props['visibility'] = 'fake'; - $this->assertIsWPError( llms_insert_access_plan( $props ) ); - $this->assertWPErrorCodeEquals( 'invalid-visibility', llms_insert_access_plan( $props ) ); - - // Valid visibilities. - foreach ( array_keys( llms_get_access_plan_visibility_options() ) as $visibility ) { - - $props['visibility'] = $visibility; - $plan = llms_insert_access_plan( $props ); - - // Success. - $this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) ); - - // Getter. - $this->assertEquals( $visibility, $plan->get_visibility() ); - - } - - } - - - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-admin.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-admin.php deleted file mode 100644 index c94c31377d..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-admin.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php -/** - * Tests for LifterLMS User Postmeta functions - * - * @group functions - * @group admin_functions - * @group admin - * - * @since 3.23.0 - */ -class LLMS_Test_Functions_Admin extends LLMS_UnitTestCase { - - /** - * Test: llms_get_add_ons() - * - * @since 4.21.3 - * - * @return void - */ - public function test_llms_get_add_ons() { - - $res = llms_get_add_ons(); - - // Return looks right. - $this->assertEquals( array( 'categories', 'items' ), array_keys( $res ) ); - - // Transient set for caching. - $this->assertEquals( $res, get_transient( 'llms_products_api_result' ) ); - - } - - /** - * Test llms_get_add_ons() when an error is encountered. - * - * @since 4.21.3 - * - * @return void - */ - public function test_llms_get_add_ons_error() { - - $err = new WP_Error( 'mocked-err', 'Mocked Message', array( 'data' => 'mocked' ) ); - $this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $err ); - - $res = llms_get_add_ons(); - - // Expect mocked error message. - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'api_connection', $res ); - $this->assertWPErrorDataEquals( $err, $res ); - - // No transient data. - $this->assertFalse( get_transient( 'llms_products_api_result' ) ); - - } - - /** - * Test: llms_get_add_ons() caching mechanisms - * - * @since 4.21.3 - * - * @return void - */ - public function test_llms_get_add_ons_with_caching() { - - $mock = array( 'mock' ); - set_transient( 'llms_products_api_result', $mock, DAY_IN_SECONDS ); - $this->assertEquals( $mock, llms_get_add_ons() ); - - // Skip cache. - $this->assertNotEquals( $mock, llms_get_add_ons( false ) ); - - } - - /** - * Test llms_get_add_on() - * - * @since 4.21.3 - * @since 5.0.0 Stop testing against Helper_Add_on. - * - * @return void - */ - public function test_llms_get_add_on() { - - // Fake add-on still works. - $this->assertTrue( llms_get_add_on( array( 'id' => 'test' ) ) instanceof LLMS_Add_On ); - - // Lookup a real add-on via a string. - $res = llms_get_add_on( 'lifterlms-com-lifterlms', 'id' ); - $this->assertEquals( 'lifterlms-com-lifterlms', $res->get( 'id' ) ); - - // // Pass in the whole add-on array. - // $res = llms_get_add_on( LLMS_Unit_Test_Util::get_private_property_value( $res, 'data' ) ); - // $this->assertEquals( 'lifterlms-com-lifterlms', $res->get( 'id' ) ); - - // // Should load the Helper's if found subclass. - // global $lifterlms_tests; - // require_once $lifterlms_tests->tests_dir . '/mocks/class-llms-helper-add-on.php'; - - // $this->assertTrue( llms_get_add_on( array( 'id' => 'test' ) ) instanceof LLMS_Helper_Add_On ); - - } - - /** - * test the llms_get_sales_page_types() function - * - * @since 3.23.0 - * - * @return void - */ - public function test_llms_get_sales_page_types() { - - $this->assertEquals( array( - 'none' => 'Display default course content', - 'content' => 'Show custom content', - 'page' => 'Redirect to WordPress Page', - 'url' => 'Redirect to custom URL', - ), llms_get_sales_page_types() ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-conditional-tags.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-conditional-tags.php deleted file mode 100644 index bddba3ae37..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-conditional-tags.php +++ /dev/null @@ -1,488 +0,0 @@ -<?php -/** - * Test Order Functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_conditional_tags - * - * @since 3.37.0 - * @since 3.37.12 Fix tests failing due to incorrect post type (It's 'llms_membership' not 'membership'). - */ -class LLMS_Test_Functions_Conditional_Tags extends LLMS_UnitTestCase { - - /** - * Test the is_course() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_course() { - - $this->assertFalse( is_course() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_course() ); - - $this->go_to( get_permalink( $this->factory->post->create() ) ); - $this->assertFalse( is_course() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) ); - $this->assertTrue( is_course() ); - - } - - /** - * Test is_course_category() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_course_category() { - - $this->assertFalse( is_course_category() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_course_category() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) ); - $this->assertFalse( is_course_category() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_course_category() ); - - // Cat not specified. - $term = wp_create_term( 'mock-cat', 'course_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_course_category() ); - $this->assertTrue( is_course_category( $term['term_id'] ) ); - $this->assertTrue( is_course_category( array( $term['term_id'] ) ) ); - - // Another term. - $term_2 = wp_create_term( 'mock-cat-2', 'course_cat' ); - $this->go_to( get_term_link( $term_2['term_id'] ) ); - $this->assertTrue( is_course_category() ); - - // We're on the other term's page. - $this->assertFalse( is_course_category( $term['term_id'] ) ); - - // One of passed terms. - $this->assertTrue( is_course_category( array( $term['term_id'], $term_2['term_id'] ) ) ); - - } - - /** - * Test is_course_tag() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_course_tag() { - - $this->assertFalse( is_course_tag() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_course_tag() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) ); - $this->assertFalse( is_course_tag() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_course_tag() ); - - // Cat not specified. - $term = wp_create_term( 'mock-cat', 'course_tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_course_tag() ); - $this->assertTrue( is_course_tag( $term['term_id'] ) ); - $this->assertTrue( is_course_tag( array( $term['term_id'] ) ) ); - - // Another term. - $term_2 = wp_create_term( 'mock-cat-2', 'course_tag' ); - $this->go_to( get_term_link( $term_2['term_id'] ) ); - $this->assertTrue( is_course_tag() ); - - // We're on the other term's page. - $this->assertFalse( is_course_tag( $term['term_id'] ) ); - - // One of passed terms. - $this->assertTrue( is_course_tag( array( $term['term_id'], $term_2['term_id'] ) ) ); - - } - - /** - * Test is_course_tag() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_course_taxonomy() { - - $this->assertFalse( is_course_taxonomy() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_course_taxonomy() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) ); - $this->assertFalse( is_course_taxonomy() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_course_taxonomy() ); - - // Cat. - $term = wp_create_term( 'mock-cat', 'course_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_course_taxonomy() ); - - // Tag. - $term = wp_create_term( 'mock-tag', 'course_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_course_taxonomy() ); - - } - - /** - * Test is_courses() - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_courses() { - - LLMS_Install::create_pages(); - - $this->assertFalse( is_courses() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_courses() ); - - $this->go_to( get_post_type_archive_link( 'llms_membership' ) ); - $this->assertFalse( is_courses() ); - - $this->go_to( get_post_type_archive_link( 'course' ) ); - $this->assertTrue( is_courses() ); - - $this->go_to( get_permalink( llms_get_page_id( 'courses' ) ) ); - $this->assertTrue( is_courses() ); - - } - - /** - * Test the is_lesson() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_lesson() { - - $this->assertFalse( is_lesson() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_lesson() ); - - $this->go_to( get_permalink( $this->factory->post->create() ) ); - $this->assertFalse( is_lesson() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) ) ); - $this->assertTrue( is_lesson() ); - - } - - /** - * Test is_lifterlms() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_lifterlms() { - - $this->assertFalse( is_lifterlms() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_lifterlms() ); - - $post_types = array( - 'post' => false, - 'course' => true, - 'lesson' => true, - 'llms_quiz' => true, - 'llms_membership' => true, - ); - foreach( $post_types as $post_type => $expect ) { - - // Single post type. - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => $post_type ) ) ) ); - $this->assertEquals( $expect, is_lifterlms() ); - - if ( ! in_array( $post_type, array( 'lesson', 'llms_quiz' ), true ) ) { - - // Archive page. - $this->go_to( get_post_type_archive_link( $post_type ) ); - $this->assertEquals( $expect, is_lifterlms(), $post_type ); - - } - - } - - $term = wp_create_term( 'mock-cat', 'course_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_lifterlms() ); - - $term = wp_create_term( 'mock-cat', 'membership_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_lifterlms() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_lifterlms() ); - - } - - /** - * Test the is_llms_account_page() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_llms_account_page() { - - LLMS_Install::create_pages(); - - $this->assertFalse( is_llms_account_page() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_llms_account_page() ); - - add_filter( 'lifterlms_is_account_page', '__return_true' ); - $this->assertTrue( is_llms_account_page() ); - remove_filter( 'lifterlms_is_account_page', '__return_true' ); - - $this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) ); - $this->assertTrue( is_llms_account_page() ); - - } - - /** - * Test the is_llms_checkout() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_llms_checkout() { - - LLMS_Install::create_pages(); - - $this->assertFalse( is_llms_checkout() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_llms_checkout() ); - - $this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) ); - $this->assertTrue( is_llms_checkout() ); - - } - - /** - * Test the is_membership() function. - * - * @since 3.37.0 - * @since 3.37.12 Fix tests failing due to incorrect post type. - * - * @return void - */ - public function test_is_membership() { - - $this->assertFalse( is_membership() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_membership() ); - - $this->go_to( get_permalink( $this->factory->post->create() ) ); - $this->assertFalse( is_membership() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) ); - $this->assertTrue( is_membership() ); - - } - - /** - * Test is_membership_category() function. - * - * @since 3.37.0 - * @since 3.37.12 Fix tests failing due to incorrect post type. - * - * @return [type] - */ - public function test_is_membership_category() { - - $this->assertFalse( is_membership_category() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_membership_category() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) ); - $this->assertFalse( is_membership_category() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_membership_category() ); - - // Cat not specified. - $term = wp_create_term( 'mock-cat', 'membership_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_membership_category() ); - $this->assertTrue( is_membership_category( $term['term_id'] ) ); - $this->assertTrue( is_membership_category( array( $term['term_id'] ) ) ); - - // Another term. - $term_2 = wp_create_term( 'mock-cat-2', 'membership_cat' ); - $this->go_to( get_term_link( $term_2['term_id'] ) ); - $this->assertTrue( is_membership_category() ); - - // We're on the other term's page. - $this->assertFalse( is_membership_category( $term['term_id'] ) ); - - // One of passed terms. - $this->assertTrue( is_membership_category( array( $term['term_id'], $term_2['term_id'] ) ) ); - - } - - /** - * Test is_membership_tag() function. - * - * @since 3.37.0 - * @since 3.37.12 Fix tests failing due to incorrect post type. - * - * @return [type] - */ - public function test_is_membership_tag() { - - $this->assertFalse( is_membership_tag() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_membership_tag() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) ); - $this->assertFalse( is_membership_tag() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_membership_tag() ); - - // Cat not specified. - $term = wp_create_term( 'mock-cat', 'membership_tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_membership_tag() ); - $this->assertTrue( is_membership_tag( $term['term_id'] ) ); - $this->assertTrue( is_membership_tag( array( $term['term_id'] ) ) ); - - // Another term. - $term_2 = wp_create_term( 'mock-cat-2', 'membership_tag' ); - $this->go_to( get_term_link( $term_2['term_id'] ) ); - $this->assertTrue( is_membership_tag() ); - - // We're on the other term's page. - $this->assertFalse( is_membership_tag( $term['term_id'] ) ); - - // One of passed terms. - $this->assertTrue( is_membership_tag( array( $term['term_id'], $term_2['term_id'] ) ) ); - - } - - /** - * Test is_membership_tag() function. - * - * @since 3.37.0 - * @since 3.37.12 Fix tests failing due to incorrect post type. - * - * @return [type] - */ - public function test_is_membership_taxonomy() { - - $this->assertFalse( is_membership_taxonomy() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_membership_taxonomy() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) ); - $this->assertFalse( is_membership_taxonomy() ); - - $term = wp_create_tag( 'mock-tag' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertFalse( is_membership_taxonomy() ); - - // Cat. - $term = wp_create_term( 'mock-cat', 'membership_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_membership_taxonomy() ); - - // Tag. - $term = wp_create_term( 'mock-tag', 'membership_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertTrue( is_membership_taxonomy() ); - - } - - /** - * Test is_memberships() - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_memberships() { - - LLMS_Install::create_pages(); - - $this->assertFalse( is_memberships() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_memberships() ); - - $this->go_to( get_post_type_archive_link( 'course' ) ); - $this->assertFalse( is_memberships() ); - - $this->go_to( get_post_type_archive_link( 'llms_membership' ) ); - $this->assertTrue( is_memberships() ); - - $this->go_to( get_permalink( llms_get_page_id( 'memberships' ) ) ); - $this->assertTrue( is_memberships() ); - - } - - /** - * Test the is_quiz() function. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_quiz() { - - $this->assertFalse( is_quiz() ); - - $this->go_to( home_url() ); - $this->assertFalse( is_quiz() ); - - $this->go_to( get_permalink( $this->factory->post->create() ) ); - $this->assertFalse( is_quiz() ); - - $this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) ) ) ); - $this->assertTrue( is_quiz() ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php deleted file mode 100644 index 04ce51dd30..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php +++ /dev/null @@ -1,459 +0,0 @@ -<?php -/** - * Tests for LifterLMS User Postmeta functions - * - * @package LifterLMS/Tests - * - * @group functions - * @group content_functions - * - * @since 3.25.1 - */ -class LLMS_Test_Functions_Content extends LLMS_UnitTestCase { - - /** - * Helper to retrieve filtered post content for a given post - * - * @since 4.17.0 - * - * @param WP_Post $post Post object - * @return string - */ - private function get_post_content( $post ) { - return trim( apply_filters( 'the_content', $post->post_content ) ); - } - - /** - * Retrieve a mock post of a give type with expected content and excerpts. - * - * @since 4.17.0 - * - * @param WP_Post $post Post object - * @return WP_Post - */ - private function get_mock_post( $post_type ) { - - global $post; - $post = $this->factory->post->create_and_get( array( - 'post_type' => $post_type, - 'post_content' => '<p>Post Content</p>', - 'post_excerpt' => '<p>Post Excerpt</p>', - ) ); - - return $post; - - } - - /** - * Callback for `llms_page_restricted` filter to force a page to look restricted - * - * @since 4.17.0 - * - * @param array $restrictions Restriction data array from llms_page_restricted(). - * @return array - */ - public function make_restricted( $restrictions ) { - $restrictions['is_restricted'] = true; - return $restrictions; - } - - /** - * Test llms_get_post_content() for various post types - * - * This test was never a very good one but it's retained as it does ensure WP core post types - * are not affected by our functions. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content() { - - llms_post_content_init(); - - $content = '<p>Lorem ipsum dolor sit amet.</p>'; - $post_types = array( 'llms_membership', 'course', 'lesson', 'llms_quiz', 'post', 'page' ); - foreach ( $post_types as $post_type ) { - - global $post; - $post = $this->factory->post->create_and_get( array( - 'post_type' => $post_type, - 'post_content' => $content, - ) ); - - if ( in_array( $post_type, array( 'post', 'page', 'llms_membership' ), true ) ) { - $this->assertEquals( $content, $this->get_post_content( $post ) ); - } else { - $this->assertNotEquals( $content, $this->get_post_content( $post ) ); - } - - } - - } - - /** - * Test llms_get_post_content() for the course post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_course_restricted_no_sales_page() { - - $before = did_action( 'lifterlms_single_course_before_summary' ); - $after = did_action( 'lifterlms_single_course_after_summary' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'course' ); - - $res = $this->get_post_content( $post ); - - // Starts with the default post content. - $this->assertSame( 0, strpos( $res, '<p>Post Content</p>' ) ); - - // Additions added to the end. - $additions = array( - '<div class="llms-meta-info">', - '<section class="llms-instructor-info">', - '<div class="llms-syllabus-wrapper">', - ); - foreach ( $additions as $add ) { - $this->assertStringContains( $add, $res ); - } - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) ); - - } - - /** - * Test llms_get_post_content() for the course post type with restrictions and a salse page. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_course_restricted_with_sales_page() { - - $before = did_action( 'lifterlms_single_course_before_summary' ); - $after = did_action( 'lifterlms_single_course_after_summary' ); - - add_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'course' ); - - update_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' ); - - $res = $this->get_post_content( $post ); - - // Starts with the post's excerpt post content. - $this->assertSame( 0, strpos( $res, '<p>Post Excerpt</p>' ) ); - - // Post's content should not be found. - $this->assertSame( false, strpos( $res, '<p>Post Content</p>' ) ); - - // Additions added to the end. - $additions = array( - '<div class="llms-meta-info">', - '<section class="llms-instructor-info">', - '<div class="llms-syllabus-wrapper">', - ); - foreach ( $additions as $add ) { - $this->assertStringContains( $add, $res ); - } - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) ); - - remove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - } - - /** - * Test llms_get_post_content() for the membership post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_membership_restricted_no_sales_page() { - - $before = did_action( 'lifterlms_single_membership_before_summary' ); - $after = did_action( 'lifterlms_single_membership_after_summary' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'llms_membership' ); - - $res = $this->get_post_content( $post ); - - // No additions to the post content. - $this->assertEquals( '<p>Post Content</p>', $res ); - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) ); - - } - - /** - * Test llms_get_post_content() for the membership post type with restrictions and a salse page. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_membership_restricted_with_sales_page() { - - $before = did_action( 'lifterlms_single_membership_before_summary' ); - $after = did_action( 'lifterlms_single_membership_after_summary' ); - - $handler = function( $restrictions ) { - $restrictions['is_restricted'] = true; - return $restrictions; - }; - add_filter( 'llms_page_restricted', $handler ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'llms_membership' ); - - update_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' ); - - $res = $this->get_post_content( $post ); - - // Just the excerpt. - $this->assertEquals( '<p>Post Excerpt</p>', $res ); - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) ); - - remove_filter( 'llms_page_restricted', $handler ); - - } - - /** - * Test llms_get_post_content() for the lesson post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_lesson() { - - $before = did_action( 'lifterlms_single_lesson_before_summary' ); - $after = did_action( 'lifterlms_single_lesson_after_summary' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'lesson' ); - - $res = $this->get_post_content( $post ); - - // Starts with the back to course link. - $this->assertSame( 0, strpos( $res, '<p class="llms-parent-course-link">' ) ); - - $additions = array( - '<p>Post Content</p>', // Default content. - '<nav class="llms-course-navigation">', - ); - foreach ( $additions as $add ) { - $this->assertStringContains( $add, $res ); - } - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_lesson_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_lesson_after_summary' ) ); - - } - - /** - * Test llms_get_post_content() for a restricted lesson post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_lesson_restricted() { - - add_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - $before = did_action( 'lifterlms_no_access_main_content' ); - $after = did_action( 'lifterlms_no_access_after' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'lesson' ); - - $res = $this->get_post_content( $post ); - - $this->assertSame( '', $res ); - - $this->assertEquals( ++$before, did_action( 'lifterlms_no_access_main_content' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_no_access_after' ) ); - - remove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - } - - /** - * Test llms_get_post_content() for the quiz post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_quiz() { - - $before = did_action( 'lifterlms_single_quiz_before_summary' ); - $after = did_action( 'lifterlms_single_quiz_after_summary' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'llms_quiz' ); - - $res = $this->get_post_content( $post ); - - // Starts with a wrapper. - $this->assertSame( 0, strpos( $res, '<div class="llms-quiz-wrapper" id="llms-quiz-wrapper">' ) ); - - $additions = array( - '<div class="llms-return">', - '<p>Post Content</p>', // Default content. - '</div><!--end #llms-quiz-wrapper -->', - ); - foreach ( $additions as $add ) { - $this->assertStringContains( $add, $res ); - } - - $this->assertEquals( ++$before, did_action( 'lifterlms_single_quiz_before_summary' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_single_quiz_after_summary' ) ); - - } - - /** - * Test llms_get_post_content() for a restricted quiz post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_quiz_restricted() { - - add_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - $before = did_action( 'lifterlms_no_access_main_content' ); - $after = did_action( 'lifterlms_no_access_after' ); - - llms_post_content_init(); - $post = $this->get_mock_post( 'llms_quiz' ); - - $res = $this->get_post_content( $post ); - - $this->assertSame( '', $res ); - - $this->assertEquals( ++$before, did_action( 'lifterlms_no_access_main_content' ) ); - $this->assertEquals( ++$after, did_action( 'lifterlms_no_access_after' ) ); - - remove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) ); - - } - - /** - * Test that llms_get_post_content() will return early if the `$post` global is not set. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_content_no_global() { - - llms_post_content_init(); - - $input = 'whatever'; - $this->assertEquals( $input, llms_get_post_content( $input ) ); - - } - - /** - * Test llms_get_post_sales_page_content() for an unsupported post type. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_sales_page_content_unsupported() { - $this->assertEquals( 'default content', llms_get_post_sales_page_content( $this->factory->post->create_and_get(), 'default content' ) ); - } - - /** - * Test llms_get_post_sales_page_content() for supported post types. - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_get_post_sales_page_content_supported() { - - $post_excerpt = 'excerpt content'; - - foreach ( array( 'course', 'llms_membership' ) as $post_type ) { - - $post = $this->factory->post->create_and_get( compact( 'post_type', 'post_excerpt' ) ); - update_post_meta( $post->ID, '_llms_sales_page_content_type', 'redirect' ); - $this->assertEquals( 'default content', llms_get_post_sales_page_content( $post, 'default content' ) ); - - update_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' ); - - $this->assertEquals( "<p>excerpt content</p>\n", llms_get_post_sales_page_content( $post, 'default content' ) ); - } - - } - - /** - * Test llms_post_content_init() when filters should be applied - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_post_content_init() { - - remove_filter( 'the_content', 'llms_get_post_content' ); - - $this->assertTrue( llms_post_content_init() ); - $this->assertEquals( 10, has_filter( 'the_content', 'llms_get_post_content' ) ); - - } - - /** - * Test llms_post_content_init() when on the admin panel - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_post_content_init_is_admin() { - - remove_filter( 'the_content', 'llms_get_post_content' ); - - set_current_screen( 'admin.php' ); - - $this->assertFalse( llms_post_content_init() ); - $this->assertFalse( has_filter( 'the_content', 'llms_get_post_content' ) ); - - set_current_screen( 'front' ); // Reset. - - } - - /** - * Test llms_post_content_init() when filters should be applied - * - * @since 4.17.0 - * - * @return void - */ - public function test_llms_post_content_custom() { - - $this->assertTrue( llms_post_content_init( 'a_fake_callback', 85 ) ); - $this->assertEquals( 85, has_filter( 'the_content', 'a_fake_callback' ) ); - - remove_filter( 'the_content', 'a_fake_callback' ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-core.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-core.php deleted file mode 100644 index 260c324782..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-core.php +++ /dev/null @@ -1,964 +0,0 @@ -<?php -/** - * Tests for LifterLMS Core Functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_core - * - * @since 3.3.1 - * @since 3.35.0 Test ipv6 addresses. - * @since 3.36.1 Use exception from lifterlms-tests lib. - * @since 3.37.12 Fix errors thrown due to usage of `llms_section` instead of `section`. - * @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`. - * @since 4.2.0 Add tests for llms_get_completable_post_types() & llms_get_completable_taxonomies(). - * @since 4.4.0 Add tests for `llms_deprecated_function()`. - * @since 4.4.1 Add tests for `llms_get_enrollable_post_types()` and `llms_get_enrollable_status_check_post_types()`. - * @since 4.7.0 Add test for `llms_get_dom_document()`. - * @since 4.10.1 Add test for possible 3rd party cpts conflicts using `llms_get_post()`. - * @since 4.13.0 Test `llms_get_dom_document()` relying on `mb_convert_encoding()` and not. - */ -class LLMS_Test_Functions_Core extends LLMS_UnitTestCase { - - /** - * Test the llms_assoc_array_insert - * - * @since 3.21.0 - * - * @return void - */ - public function test_llms_assoc_array_insert() { - - // base array. - $array = array( - 'test' => 'asrt', - 'tester' => 'asrtarst', - 'moretest_key' => 'arst', - 'another' => 'arst', - ); - - // after first item. - $expect = array( - 'test' => 'asrt', - 'new_key' => 'item', - 'tester' => 'asrtarst', - 'moretest_key' => 'arst', - 'another' => 'arst', - ); - $this->assertEquals( $expect, llms_assoc_array_insert( $array, 'test', 'new_key', 'item' ) ); - - // add in the middle. - $expect = array( - 'test' => 'asrt', - 'tester' => 'asrtarst', - 'new_key' => 'item', - 'moretest_key' => 'arst', - 'another' => 'arst', - ); - $this->assertEquals( $expect, llms_assoc_array_insert( $array, 'tester', 'new_key', 'item' ) ); - - // requested key doesn't exist so it'll be added to the end. - $expect = array( - 'test' => 'asrt', - 'tester' => 'asrtarst', - 'moretest_key' => 'arst', - 'another' => 'arst', - 'new_key' => 'item', - ); - $this->assertEquals( $expect, llms_assoc_array_insert( $array, 'noexist', 'new_key', 'item' ) ); - - // after last item. - $expect = array( - 'test' => 'asrt', - 'new_key' => 'item', - 'tester' => 'asrtarst', - 'moretest_key' => 'arst', - 'another' => 'arst', - ); - $this->assertEquals( $expect, llms_assoc_array_insert( $array, 'another', 'new_key', 'item' ) ); - - } - - /** - * Test llms_deprecated_function() - * - * @since 4.4.0 - * - * @expectedDeprecated DEPRECATED - * - * @return void - */ - public function test_llms_deprecated_function() { - - // Add an action where we'll test that all our deprecation data is properly passed. - add_action( 'deprecated_function_run', array( $this, 'deprecated_function_run_assertions' ), 10, 3 ); - - llms_deprecated_function( 'DEPRECATED', '999.999.999', 'REPLACEMENT' ); - - remove_action( 'deprecated_function_run', array( $this, 'deprecated_function_run_assertions' ) ); - - } - - /** - * Callback method used to test `llms_deprecated_function()`. - * - * @since 4.4.0 - * - * @param string $function Deprecated function name. - * @param string $replacement Deprecated function replacement. - * @param string $version Deprecated version number. - * @return void - */ - public function deprecated_function_run_assertions( $function, $replacement, $version ) { - - // Our deprecation data should be passed to the core. - $this->assertEquals( 'DEPRECATED', $function ); - $this->assertEquals( 'REPLACEMENT', $replacement ); - $this->assertEquals( '999.999.999', $version ); - - } - - /** - * Test llms_get_completable_post_types() - * - * @since 4.2.0 - * - * @return void - */ - public function test_llms_get_completable_post_types() { - $this->assertEquals( array( 'course', 'section', 'lesson' ), llms_get_completable_post_types() ); - } - - /** - * Test llms_get_completable_taxonomies() - * - * @since 4.2.0 - * - * @return void - */ - public function test_llms_get_completable_taxonomies() { - $this->assertEquals( array( 'course_track' ), llms_get_completable_taxonomies() ); - } - - - /** - * Test llms_get_core_supported_themes() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_core_supported_themes() { - - $this->assertFalse( empty( llms_get_core_supported_themes() ) ); - $this->assertTrue( is_array( llms_get_core_supported_themes() ) ); - - } - - /** - * Test llms_get_date_diff() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_date_diff() { - - $this->assertEquals( '18 days', llms_get_date_diff( '2016-05-12', '2016-05-30' ) ); - $this->assertEquals( '1 year, 2 months', llms_get_date_diff( '2016-01-01', '2017-03-25 23:32:32' ) ); - $this->assertEquals( '10 months, 14 days', llms_get_date_diff( '2016-01-01', '2016-11-15' ) ); - $this->assertEquals( '4 years, 24 days', llms_get_date_diff( '2013-03-01', '2017-03-25' ) ); - $this->assertEquals( '3 years, 10 months', llms_get_date_diff( '2013-03-01', '2017-01-25' ) ); - $this->assertEquals( '24 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:01:25' ) ); - $this->assertEquals( '1 second', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:01:02' ) ); - $this->assertEquals( '59 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:00' ) ); - $this->assertEquals( '1 minute, 44 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:45' ) ); - $this->assertEquals( '1 minute, 14 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:15' ) ); - $this->assertEquals( '3 minutes, 59 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:05:00' ) ); - $this->assertEquals( '44 minutes, 33 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:45:34' ) ); - $this->assertEquals( '44 minutes, 33 seconds', llms_get_date_diff( '2016-05-12 01:45:34', '2016-05-12 01:01:01' ) ); - - } - - /** - * Test llms_get_dom_document() - * - * @since 4.7.0 - * @since 4.8.0 Test against HTML strings, HTML documents, strings with character entities, and strings with non-utf8 characters. - * @since 4.13.0 Test `llms_get_dom_document()` relying on `mb_convert_encoding()` and not. - * Also, use `$this->assertStringContainsString()` in place of `$this->assertStringContainsString()` to get a better erro message on failures. - * - * @return void - */ - public function test_llms_get_dom_document() { - - /** - * Array of test strings - * - * First value is the input string & the second value is the expected output string. - * - * @var array[] - */ - $tests = array( - array( - 'simple text string', - '<p>simple text string</p>', - ), - array( - '<h1>html text string</h1><br><div class="test"><em>wow!</em></div>', - '<h1>html text string</h1><br><div class="test"><em>wow!</em></div>', - ), - array( - 'Ḷ𝝄𝔯𝚎ɱ ĭ𝓹ᵴǘɱ ժөḻ𝝈ɍ 𝘀𝗂ᴛ.', - '<p>Ḷ𝝄𝔯𝚎ɱ ĭ𝓹ᵴǘɱ ժөḻ𝝈ɍ 𝘀𝗂ᴛ.</p>', - ), - array( - 'Contains — Char Codes and special – !', - '<p>Contains — Char Codes and special – !</p>', - ), - array( - '<!DOCTYPE html><html lang="en-US"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width" /></head><body>And >>> a <b>full</b> HTML docum𝞔nt!</body></html>', - 'And >>> a <b>full</b> HTML docum𝞔nt!', - ), - ); - - // Using `mb_convert_econding()`. - foreach ( $tests as $test ) { - - $dom = llms_get_dom_document( $test[0] ); - $this->assertTrue( $dom instanceof DOMDocument, $test[1] ); - $this->assertStringContainsString( sprintf( '<body>%s</body></html>', $test[1] ), $dom->saveHTML() ); - - } - - // Repeat the same test using "the meta fixer". - add_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false' ); - - foreach ( $tests as $test ) { - - $dom = llms_get_dom_document( $test[0] ); - $this->assertTrue( $dom instanceof DOMDocument, $test[1] ); - $this->assertStringContainsString( sprintf( '<body>%s</body></html>', $test[1] ), $dom->saveHTML() ); - - } - - remove_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false' ); - } - - /** - * Test llms_get_engagement_triggers() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_engagement_triggers() { - $this->assertFalse( empty( llms_get_engagement_triggers() ) ); - $this->assertTrue( is_array( llms_get_engagement_triggers() ) ); - } - - /** - * Test llms_get_engagement_types() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_engagement_types() { - $this->assertFalse( empty( llms_get_engagement_types() ) ); - $this->assertTrue( is_array( llms_get_engagement_types() ) ); - } - - /** - * Test llms_get_enrollable_post_types() - * - * @since 4.4.1 - * - * @return void - */ - public function test_llms_get_enrollable_post_types() { - foreach ( llms_get_enrollable_post_types() as $post_type ) { - $this->assertTrue( is_string( $post_type ) ); - $this->assertTrue( post_type_exists( $post_type ) ); - } - } - - /** - * Test llms_get_enrollable_status_check_post_types() - * - * @since 4.4.1 - * - * @return void - */ - public function test_llms_get_enrollable_status_check_post_types() { - foreach ( llms_get_enrollable_status_check_post_types() as $post_type ) { - $this->assertTrue( is_string( $post_type ) ); - $this->assertTrue( post_type_exists( $post_type ) ); - } - } - - /** - * Test llms_get_open_registration_status() - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_open_registration_status() { - - // No value, defaults to no. - delete_option( 'lifterlms_enable_myaccount_registration' ); - $this->assertEquals( 'no', llms_get_open_registration_status() ); - - // Explicitly no. - update_option( 'lifterlms_enable_myaccount_registration', 'no' ); - $this->assertEquals( 'no', llms_get_open_registration_status() ); - - // Explicitly yes. - update_option( 'lifterlms_enable_myaccount_registration', 'yes' ); - $this->assertEquals( 'yes', llms_get_open_registration_status() ); - - // Explicitly yes but filtered off. - $handler = function( $val ) { - return 'no'; - }; - add_filter( 'llms_enable_open_registration', $handler ); - $this->assertEquals( 'no', llms_get_open_registration_status() ); - remove_filter( 'llms_enable_open_registration', $handler ); - - } - - /** - * Test the llms_get_option_page_anchor() function - * - * @since 3.19.0 - * - * @return void - */ - public function test_llms_get_option_page_anchor() { - - $id = $this->factory->post->create( array( - 'post_title' => 'The Page Title', - 'post_type' => 'page', - ) ); - - $option_name = 'llms_test_page_anchor'; - - // returns empty if option isn't set. - $this->assertEmpty( llms_get_option_page_anchor( $option_name ) ); - - update_option( $option_name, $id ); - - // title found in string. - $this->assertTrue( false !== strpos( llms_get_option_page_anchor( $option_name ), get_the_title( $id ) ) ); - - // URL found. - $this->assertTrue( false !== strpos( llms_get_option_page_anchor( $option_name ), get_the_permalink( $id ) ) ); - - // no target found. - $this->assertTrue( false === strpos( llms_get_option_page_anchor( $option_name, false ), 'target="_blank"' ) ); - - } - - /** - * Test llms_get_product_visibility_options() - * - * @since 3.6.0 - * - * @return void - */ - public function test_llms_get_product_visibility_options() { - $this->assertFalse( empty( llms_get_product_visibility_options() ) ); - $this->assertTrue( is_array( llms_get_product_visibility_options() ) ); - } - - /** - * Test llms_filter_input_sanitize_string() when the input var isn't set. - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_filter_input_sanitize_string_var_not_set() { - - $this->assertNull( llms_filter_input_sanitize_string( INPUT_POST, uniqid( 'notset_' ) ) ); - $this->assertNull( llms_filter_input_sanitize_string( INPUT_POST, uniqid( 'notset_' ), array( FILTER_REQUIRE_ARRAY ) ) ); - - } - - - /** - * Test llms_filter_input_sanitize_string() when the input var is "empty". - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_filter_input_sanitize_string_var_empty() { - - $tests = array( - - array( - '', - '', - ), - array( - false, - false, - ), - array( - '0', - '0', - ), - array( - null, - null, - ), - ); - - foreach ( $tests as $test ) { - list( $input, $output ) = $test; - $this->mockPostRequest( compact( 'input' ) ); - $this->assertEquals( $output, llms_filter_input_sanitize_string( INPUT_POST, 'input' ) ); - } - - } - - /** - * Test llms_filter_input_sanitize_string(). - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_filter_input_sanitize_string() { - - $tests = array( - array( - 'simple text input', // Input. - 'simple text input', // Output with quotes encoded. - 'simple text input', // Output without quotes encoded. - ), - array( - 'input "with" double quotes.', - 'input "with" double quotes.', - 'input "with" double quotes.', - ), - array( - "input 'with' single quotes.", - "input 'with' single quotes.", - "input 'with' single quotes.", - ), - array( - '<a href="#">Solo Tag</a>', - 'Solo Tag', - 'Solo Tag', - ), - array( - 'Text and <a href="#">a tag</a> and more text', - 'Text and a tag and more text', - 'Text and a tag and more text', - ), - array( - 'Text and <a href="#">a tag</a> and <b>more tags</b> and "quotes".', - 'Text and a tag and more tags and "quotes".', - 'Text and a tag and more tags and "quotes".', - ), - array( - 1, - '1', - '1', - ), - array( - true, - '1', - '1', - ), - array( - '234234', - '234234', - '234234', - ), - array( - 'true', - 'true', - 'true', - ), - array( - 'false', - 'false', - 'false', - ), - array( - 'null', - 'null', - 'null', - ), - ); - - $types = array( - INPUT_GET => 'mockGetRequest', - INPUT_POST => 'mockPostRequest', - ); - foreach ( $types as $type => $mock_func ) { - - // Setup FILTER_REQUIRE_ARRAY vars. - $arr_input = array(); - $arr_output = array(); - $arr_output_no_encode = array(); - - foreach ( $tests as $test ) { - - list( $input, $output, $output_no_encode ) = $test; - $this->$mock_func( compact( 'input' ) ); - - // Test input with quotes encoded. - $this->assertEquals( $output, llms_filter_input_sanitize_string( $type, 'input' ), "Input string: {$input}" ); - - // Quotes not encoded. - $this->assertEquals( $output_no_encode, llms_filter_input_sanitize_string( $type, 'input', array( FILTER_FLAG_NO_ENCODE_QUOTES ) ), "Input string: {$input}" ); - - // Requesting array when no array submitted results in the filter failing. - $this->assertFalse( llms_filter_input_sanitize_string( $type, 'input', array( FILTER_REQUIRE_ARRAY ) ), "Input string: {$input}" ); - - // Add to FILTER_REQUIRE_ARRAY vars. - $arr_input[] = $input; - $arr_output[] = $output; - $arr_output_no_encode[] = $output_no_encode; - - } - - // Test array-related input. - $this->$mock_func( compact( 'arr_input' ) ); - - // Array submitted but FILTER_REQUIRE_ARRAY not passed as an option. - $this->assertEquals( '', llms_filter_input_sanitize_string( $type, 'arr_input' ) ); - - // Array requested. - $this->assertEquals( $arr_output, llms_filter_input_sanitize_string( $type, 'arr_input', array( FILTER_REQUIRE_ARRAY ) ) ); - $this->assertEquals( $arr_output_no_encode, llms_filter_input_sanitize_string( $type, 'arr_input', array( FILTER_REQUIRE_ARRAY, FILTER_FLAG_NO_ENCODE_QUOTES ) ) ); - - } - - } - - /** - * Test llms_find_coupon() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_find_coupon() { - - // create a coupon. - $id = $this->factory->post->create( array( - 'post_title' => 'coopond', - 'post_type' => 'llms_coupon', - ) ); - $this->assertEquals( $id, llms_find_coupon( 'coopond' ) ); - - // create a dup. - $dup = $this->factory->post->create( array( - 'post_title' => 'coopond', - 'post_type' => 'llms_coupon', - ) ); - $this->assertEquals( $dup, llms_find_coupon( 'coopond' ) ); - - // test dupcheck. - $this->assertEquals( $id, llms_find_coupon( 'coopond', $dup ) ); - - // delete the coupon. - wp_delete_post( $id ); - wp_delete_post( $dup ); - $this->assertEmpty( llms_find_coupon( 'coopond' ) ); - - } - - /** - * Test llms_get_enrolled_students() - * - * @since 3.6.0 - * @return void - */ - function test_llms_get_enrolled_students() { - - $course_id = $this->factory->post->create( array( - 'post_type' => 'course', - ) ); - - $students = $this->factory->user->create_many( 25, array( 'role' => 'student' ) ); - $students_copy = $students; - foreach ( $students as $student_id ) { - $student = new LLMS_Student( $student_id ); - $student->enroll( $course_id ); - } - - // test basic enrollment query passing in a string. - $this->assertEquals( $students, llms_get_enrolled_students( $course_id, 'enrolled', 50, 0 ) ); - // test basic enrollment query passing in an array. - $this->assertEquals( $students, llms_get_enrolled_students( $course_id, array( 'enrolled' ), 50, 0 ) ); - - // test pagination. - $this->assertEquals( array_splice( $students, 0, 10 ), llms_get_enrolled_students( $course_id, 'enrolled', 10, 0 ) ); - $this->assertEquals( array_splice( $students, 0, 10 ), llms_get_enrolled_students( $course_id, 'enrolled', 10, 10 ) ); - $this->assertEquals( $students, llms_get_enrolled_students( $course_id, 'enrolled', 10, 20 ) ); - - // should be no one expired. - $this->assertEquals( array(), llms_get_enrolled_students( $course_id, 'expired', 10, 0 ) ); - - // sleeping makes unenrollment tests work. - sleep( 1 ); - - $i = 0; - $expired = array(); - while ( $i < 5 ) { - $student = new LLMS_Student( $students_copy[ $i ] ); - $student->unenroll( $course_id, 'any', 'expired' ); - $expired[] = $students_copy[ $i ]; - $i++; - } - - // test expired alone. - $this->assertEquals( $expired, llms_get_enrolled_students( $course_id, 'expired', 10, 0 ) ); - - // test multiple statuses. - $this->assertEquals( $students_copy, llms_get_enrolled_students( $course_id, array( 'enrolled', 'expired' ), 50, 0 ) ); - - } - - /** - * Test llms_get_enrollment_statuses() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_enrollment_statuses() { - $this->assertFalse( empty( llms_get_enrollment_statuses() ) ); - $this->assertTrue( is_array( llms_get_enrollment_statuses() ) ); - } - - /** - * Test llms_get_enrollment_status_name() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_enrollment_status_name() { - $this->assertNotEquals( 'asrt', llms_get_enrollment_status_name( 'cancelled' ) ); - $this->assertNotEquals( 'cancelled', llms_get_enrollment_status_name( 'Cancelled' ) ); - $this->assertEquals( 'Cancelled', llms_get_enrollment_status_name( 'cancelled' ) ); - $this->assertEquals( 'Cancelled', llms_get_enrollment_status_name( 'Cancelled' ) ); - $this->assertEquals( 'wut', llms_get_enrollment_status_name( 'wut' ) ); - } - - /** - * Test llms_get_ip_address() - * - * @since 3.6.0 - * @since 3.35.0 Test sanitization and ipv6 addresses. - * - * @return void - */ - public function test_llms_get_ip_address() { - - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; - $this->assertEquals( '127.0.0.1', llms_get_ip_address() ); - - $_SERVER['REMOTE_ADDR'] = '::1'; - $this->assertEquals( '::1', llms_get_ip_address() ); - unset( $_SERVER['REMOTE_ADDR'] ); - - $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1, 192.168.1.1, 192.168.1.5'; - $this->assertEquals( '127.0.0.1', llms_get_ip_address() ); - - $_SERVER['HTTP_X_FORWARDED_FOR'] = '::1, ::2'; - $this->assertEquals( '::1', llms_get_ip_address() ); - unset( $_SERVER['HTTP_X_FORWARDED_FOR'] ); - - $_SERVER['HTTP_X_REAL_IP'] = '127.0.0.1'; - $this->assertEquals( '127.0.0.1', llms_get_ip_address() ); - - $_SERVER['HTTP_X_REAL_IP'] = '::1'; - $this->assertEquals( '::1', llms_get_ip_address() ); - unset( $_SERVER['HTTP_X_REAL_IP'] ); - - $this->assertEquals( '', llms_get_ip_address() ); - - $_SERVER['REMOTE_ADDR'] = '127\.0.0.1'; - $this->assertEquals( '127.0.0.1', llms_get_ip_address() ); - - $_SERVER['REMOTE_ADDR'] = '127\\/\/\/\.0.0.1'; - $this->assertEquals( '', llms_get_ip_address() ); - - } - - /** - * Test llms_get_post() - * - * @since 3.3.1 - * @since 3.16.11 Unknown. - * @since 3.37.12 Fix errors thrown due to usage of `llms_section` instead of `section`. - * - * @return void - */ - public function test_llms_get_post() { - - $types = array( - 'LLMS_Access_Plan' => 'llms_access_plan', - 'LLMS_Coupon' => 'llms_coupon', - 'LLMS_Course' => 'course', - 'LLMS_Lesson' => 'lesson', - 'LLMS_Membership' => 'llms_membership', - 'LLMS_Order' => 'llms_order', - 'LLMS_Quiz' => 'llms_quiz', - 'LLMS_Question' => 'llms_question', - 'LLMS_Section' => 'section', - 'LLMS_Transaction' => 'llms_transaction', - ); - - foreach ( $types as $class => $type ) { - - $id = $this->factory->post->create( array( - 'post_type' => $type, - ) ); - $this->assertInstanceOf( $class, llms_get_post( $id ) ); - - } - - $this->assertInstanceOf( 'WP_Post', llms_get_post( $this->factory->post->create(), 'post' ) ); - $this->assertNull( llms_get_post( 'fail' ) ); - $this->assertNull( llms_get_post( 0 ) ); - - } - - /** - * Test llms_get_post() with post types which don't have to be confused with LifterLMS post types - * - * @since 4.10.1 - * - * @return void - */ - public function test_llms_get_post_no_conflicts() { - - $types = array( - 'LLMS_Events' => 'events', - 'LLMS_Certificate' => 'certificate', - 'LLMS_Transaction' => 'transaction', - ); - - foreach ( $types as $class => $type ) { - register_post_type( $type ); - $id = $this->factory->post->create( array( - 'post_type' => $type, - ) ); - - $this->assertNotInstanceOf( $class, llms_get_post( $id ) ); - unregister_post_type( $type ); - } - - } - - /** - * Test `llms_get_post_parent_course()` - * - * @since 3.6.0 - * @since 3.37.14 Added tests on other LLMS post types which are not instance of `LLMS_Post_Model`. - * - * @return void - */ - public function test_llms_get_post_parent_course() { - - $course = new LLMS_Course( 'new', 'title' ); - $section = new LLMS_Section( 'new', array( - 'post_title' => 'section', - 'meta_input' => array( - '_llms_parent_course' => $course->get( 'id' ) - ), - ) ); - $lesson = new LLMS_Lesson( 'new', array( - 'post_title' => 'lesson', - 'meta_input' => array( - '_llms_parent_course' => $course->get( 'id' ), - '_llms_parent_section' => $section->get( 'id' ), - ), - ) ); - - foreach ( array( $section, $lesson ) as $obj ) { - - $post = get_post( $obj->get( 'id' ) ); - - // pass in post id. - $this->assertEquals( $course, llms_get_post_parent_course( $post->ID ) ); - - // pass in an object. - $this->assertEquals( $course, llms_get_post_parent_course( $post ) ); - - } - - // other non lms post types don't have a parent course. - $reg_post = $this->factory->post->create(); - $this->assertNull( llms_get_post_parent_course( $reg_post ) ); - - // make sure an LLMS post type, which is not an istance of `LLMS_Post_Model` doesn't have a parent course. - // and no fatals are produced. - $certificate_post = $this->factory->post->create( - array( - 'post_type' => 'llms_certificate', - ) - ); - $this->assertNull( llms_get_post_parent_course( $certificate_post ) ); - } - - - /** - * Test llms_get_transaction_statuses() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_transaction_statuses() { - $this->assertFalse( empty( llms_get_transaction_statuses() ) ); - $this->assertTrue( is_array( llms_get_transaction_statuses() ) ); - } - - /** - * Test llms_is_site_https() - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_is_site_https() { - update_option( 'home', 'https://is.ssl' ); - $this->assertTrue( llms_is_site_https() ); - - update_option( 'home', 'http://is.ssl' ); - $this->assertFalse( llms_is_site_https() ); - } - - /** - * Test the llms_parse_bool function - * - * @since 3.19.0 - * - * @return void - */ - public function test_llms_parse_bool() { - - $true = array( 'yes', 'on', true, 1, 'true', '1' ); - - foreach ( $true as $val ) { - $this->assertTrue( llms_parse_bool( $val ) ); - } - - $false = array( 'no', 'off', false, 0, 'false', 'something', '', null, '0', array(), array( 'ast' ), array( true ) ); - - foreach ( $false as $val ) { - $this->assertFalse( llms_parse_bool( $val ) ); - } - - } - - /** - * Test llms_php_error_constant_to_code() - * - * @since 4.9.0 - * - * @return void - */ - public function test_llms_php_error_constant_to_code() { - - $errors = array( - E_ERROR => 'E_ERROR', - E_WARNING => 'E_WARNING', - E_PARSE => 'E_PARSE', - E_NOTICE => 'E_NOTICE', - E_CORE_ERROR => 'E_CORE_ERROR', - E_CORE_WARNING => 'E_CORE_WARNING', - E_COMPILE_ERROR => 'E_COMPILE_ERROR', - E_COMPILE_WARNING => 'E_COMPILE_WARNING', - E_USER_ERROR => 'E_USER_ERROR', - E_USER_WARNING => 'E_USER_WARNING', - E_USER_NOTICE => 'E_USER_NOTICE', - E_STRICT => 'E_STRICT', - E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', - E_DEPRECATED => 'E_DEPRECATED', - E_USER_DEPRECATED => 'E_USER_DEPRECATED', - 9999 => 9999, - ); - - foreach ( $errors as $in => $out ) { - $this->assertEquals( $out, llms_php_error_constant_to_code( $in ) ); - } - - } - - /** - * Test llms_redirect_and_exit() func with safe on - * - * @since 3.19.4 - * @since 3.34.0 Use exception from lifterlms-tests lib. - * - * @return void - */ - public function test_llms_redirect_and_exit_safe_on() { - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( 'https://lifterlms.com [302] YES' ); - llms_redirect_and_exit( 'https://lifterlms.com' ); - - } - - /** - * Test llms_redirect_and_exit() func with safe on - * - * @since 3.36.1 Use exception from lifterlms-tests lib. - * - * @return void - */ - public function test_llms_redirect_and_exit_safe_off() { - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( 'https://lifterlms.com [302] NO' ); - llms_redirect_and_exit( 'https://lifterlms.com', array( 'safe' => false ) ); - - } - - /** - * Test llms_redirect_and_exit() func with safe custom status - * - * @since 3.36.1 Use exception from lifterlms-tests lib. - * - * @return void - */ - public function test_llms_redirect_and_exit_safe_status() { - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( 'https://lifterlms.com [301] YES' ); - llms_redirect_and_exit( 'https://lifterlms.com', array( 'status' => 301 ) ); - - } - - /** - * Test llms_trim_string() - * - * @since 3.3.1 - * @since 3.6.0 Unknown. - * - * @return void - */ - public function test_llms_trim_string() { - - $this->assertEquals( 'yasssss', llms_trim_string( 'yasssss' ) ); - $this->assertEquals( 'y...', llms_trim_string( 'yasssss', 4 ) ); - $this->assertEquals( 'ya.', llms_trim_string( 'yasssss', 3, '.' ) ); - $this->assertEquals( 'yassss$', llms_trim_string( 'yassss$s', 7, '' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-currency.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-currency.php deleted file mode 100644 index df6c74f8f8..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-currency.php +++ /dev/null @@ -1,369 +0,0 @@ -<?php -/** - * Tests for LifterLMS Currency functions - * - * @group functions - * @group currency - * - * @since 3.24.1 - * @since 5.0.0 Moved country-related function tests to locale functions test file. - */ -class LLMS_Test_Functions_Currency extends LLMS_UnitTestCase { - - /** - * test the llms_format_decimal() function - * - * @since 3.24.1 - * - * @expectedDeprecated llms_format_decimal() - * - * @return void - */ - public function test_llms_format_decimal() { - - // test the most trivial case - $this->assertEquals( 3.3333, llms_format_decimal( 3.3333 ) ); - - // test the $dp argument - $this->assertEquals( 3.33, llms_format_decimal( 3.3333 , true ) ); - - // test the $trim_zeros argument - $this->assertSame( '3.33', llms_format_decimal( '3.330' , false, true ) ); - $this->assertSame( '3', llms_format_decimal( '3.0' , false, true ) ); - - // test localized decimal formatting - update_option( 'lifterlms_price_decimal_sep', ',' ); - $this->assertSame( '3.0', llms_format_decimal( '3,0' ) ); - } - - /** - * test the get_lifterlms_currency() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_currency() { - - // test default - $this->assertEquals( 'USD', get_lifterlms_currency() ); - - // test lifterlms_country option - update_option( 'lifterlms_currency', 'GBP' ); - $this->assertEquals( 'GBP', get_lifterlms_currency() ); - - // test that the lifterlms_currency filter is applied - add_filter( 'lifterlms_currency', function() { - return 'EUR'; - } ); - $this->assertEquals( 'EUR', get_lifterlms_currency() ); - } - - /** - * test the get_lifterlms_currency() function - * - * @since 3.24.1 - * @since 5.0.0 Update language. - * - * @return void - */ - public function test_get_lifterlms_currency_name() { - - // test default - $this->assertEquals( 'United States Dollar', get_lifterlms_currency_name() ); - - // test $currency argument - $this->assertEquals( 'British Pound', get_lifterlms_currency_name( 'GBP' ) ); - - // test that the lifterlms_currency_name filter is applied - add_filter( 'lifterlms_currency_name', function( $name, $currency ) { - return sprintf( '%s (%s)', $name, $currency ); - }, 10, 2 ); - $this->assertEquals( 'United States Dollar (USD)', get_lifterlms_currency_name() ); - } - - /** - * test the get_lifterlms_currencies() function - * - * @since 3.24.1 - * @since 5.0.0 Update test to ensure result matches source data array. - * - * @return void - */ - public function test_get_lifterlms_currencies() { - - $expected = include LLMS_PLUGIN_DIR . 'languages/currencies.php'; - $this->assertEquals( $expected, get_lifterlms_currencies() ); - - } - - /** - * test the get_lifterlms_currency_symbol() function - * - * @since 3.24.1 - * @since 5.0.0 Update character entity used for the pound. - * - * @return void - */ - public function test_get_lifterlms_currency_symbol() { - - // test default - $this->assertEquals( '$', get_lifterlms_currency_symbol() ); - - // test $currency argument - $this->assertEquals( '£', get_lifterlms_currency_symbol( 'GBP' ) ); - - // test that the lifterlms_currency_symbol filter is applied - add_filter( 'lifterlms_currency_symbol', function( $currency_symbol, $currency ) { - return sprintf( '%s (%s)', $currency_symbol, $currency ); - }, 10, 2 ); - $this->assertEquals( '$ (USD)', get_lifterlms_currency_symbol() ); - } - - /** - * test the get_lifterlms_currency_symbol() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_decimals() { - - // test default - $this->assertEquals( 2, get_lifterlms_decimals() ); - - // test lifterlms_decimals option - update_option( 'lifterlms_decimals', 3 ); - $this->assertEquals( 3, get_lifterlms_decimals() ); - - // test that the lifterlms_decimals filter is applied - add_filter( 'lifterlms_decimals', function() { - return 4; - } ); - $this->assertEquals( 4, get_lifterlms_decimals() ); - } - - /** - * test the get_lifterlms_decimal_separator() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_decimal_separator() { - - // test default - $this->assertEquals( '.', get_lifterlms_decimal_separator() ); - - // test lifterlms_decimal_separator option - update_option( 'lifterlms_decimal_separator', ',' ); - $this->assertEquals( ',', get_lifterlms_decimal_separator() ); - - // test that the lifterlms_decimal_separator filter is applied - add_filter( 'lifterlms_decimal_separator', function() { - return ':'; - } ); - $this->assertEquals( ':', get_lifterlms_decimal_separator() ); - } - - /** - * test the get_lifterlms_trim_zero_decimals() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_trim_zero_decimals() { - - // test default - $this->assertEquals( 'no', get_lifterlms_trim_zero_decimals() ); - - // test lifterlms_trim_zero_decimals option - update_option( 'lifterlms_trim_zero_decimals', 'yes' ); - $this->assertEquals( 'yes', get_lifterlms_trim_zero_decimals() ); - - // test that the lifterlms_trim_zero_decimals filter is applied - add_filter( 'lifterlms_trim_zero_decimals', function() { - return 'no'; - } ); - $this->assertEquals( 'no', get_lifterlms_trim_zero_decimals() ); - } - - /** - * test the get_lifterlms_price_format() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_price_format() { - - // test default - $this->assertEquals( '%1$s%2$s', get_lifterlms_price_format() ); - - // test right option - update_option( 'lifterlms_currency_position', 'right' ); - $this->assertEquals( '%2$s%1$s', get_lifterlms_price_format() ); - - // test left_space option - update_option( 'lifterlms_currency_position', 'left_space' ); - $this->assertEquals( '%1$s %2$s', get_lifterlms_price_format() ); - - // test right_space option - update_option( 'lifterlms_currency_position', 'right_space' ); - $this->assertEquals( '%2$s %1$s', get_lifterlms_price_format() ); - - // test that the lifterlms_price_format filter is applied - add_filter( 'lifterlms_price_format', function( $format, $pos ) { - return sprintf( '%s (%s)', $format, $pos ); - }, 10, 2 ); - $this->assertEquals( '%2$s %1$s (right_space)', get_lifterlms_price_format() ); - } - - /** - * test the get_lifterlms_thousand_separator() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_get_lifterlms_thousand_separator() { - - // test default - $this->assertEquals( ',', get_lifterlms_thousand_separator() ); - - // test lifterlms_thousand_separator option - update_option( 'lifterlms_thousand_separator', '.' ); - $this->assertEquals( '.', get_lifterlms_thousand_separator() ); - - // test that the lifterlms_thousand_separator filter is applied - add_filter( 'lifterlms_thousand_separator', function() { - return ':'; - } ); - $this->assertEquals( ':', get_lifterlms_thousand_separator() ); - } - - public function test_llms_get_currency_symbols() { - - $expected = include LLMS_PLUGIN_DIR . 'languages/currency-symbols.php'; - $res = llms_get_currency_symbols(); - $this->assertEquals( $expected, $res ); - - // Make sure entities decode to what's expected. - $this->assertEquals( '$', html_entity_decode( $res['USD'] ) ); - $this->assertEquals( '£', html_entity_decode( $res['GBP'] ) ); - $this->assertEquals( '€', html_entity_decode( $res['EUR'] ) ); - - // Text symbols. - $this->assertEquals( 'P', $res['BWP'] ); - $this->assertEquals( 'CHf', $res['CHF'] ); - - } - - /** - * test the llms_price() function - * - * @since 3.24.1 - * @since 5.0.0 Update currency symbol entities. - * - * @return void - */ - public function test_llms_price() { - - // test default positive price - $this->assertEquals( '<span class="lifterlms-price"><span class="llms-price-currency-symbol">$</span>2.99</span>', llms_price( 2.99 ) ); - - // test default negative price - $this->assertEquals( '<span class="lifterlms-price">-<span class="llms-price-currency-symbol">$</span>2.99</span>', llms_price( -2.99 ) ); - - // test that raw_lifterlms_price filter is applied - add_filter( 'raw_lifterlms_price', function( $price ) { - return $price * 10; - } ); - $this->assertEquals( '<span class="lifterlms-price"><span class="llms-price-currency-symbol">$</span>29.90</span>', llms_price( 2.99 ) ); - remove_all_filters( 'raw_lifterlms_price' ); - - // test that formatted_lifterlms_price filter is applied - add_filter( 'formatted_lifterlms_price', function( $formatted_price, $price, $decimals, $decimal_separator, $thousand_separator ) { - $price = number_format( $price, $decimals, $decimal_separator, $thousand_separator ); - return round( $formatted_price ); - }, 10, 5 ); - $this->assertEquals( '<span class="lifterlms-price"><span class="llms-price-currency-symbol">$</span>3</span>', llms_price( 2.99 ) ); - remove_all_filters( 'formatted_lifterlms_price' ); - - // test that llms_price filter is applied - add_filter( 'llms_price', function( $r, $price, $args ) { - return $price; - }, 10, 3 ); - $this->assertEquals( '2.99', llms_price( 2.99 ) ); - remove_all_filters( 'llms_price' ); - - // test with custom options - update_option( 'lifterlms_decimal_separator', ',' ); - update_option( 'lifterlms_decimals', 3 ); - update_option( 'lifterlms_currency_position', 'left_space' ); - update_option( 'lifterlms_thousand_separator', '.' ); - update_option( 'lifterlms_trim_zero_decimals', 'yes' ); - $this->assertEquals( '<span class="lifterlms-price"><span class="llms-price-currency-symbol">$</span> 1.002</span>', llms_price( 1002.00 ) ); - - // test with custom options via $args argument - $args = array( - 'currency' => 'GBP', - 'decimal_separator' => '.', - 'decimals' => 2, - 'format' => '<div>%s</div>%s', - 'thousand_separator' => ',', - 'trim_zeros' => 'no', - ); - $this->assertEquals( '<span class="lifterlms-price"><div><span class="llms-price-currency-symbol">£</span></div>1,003.00</span>', llms_price( '1002.999', $args ) ); - - // test with custom arguments via llms_price_args filter - add_filter( 'llms_price_args', function() { - return array( - 'currency' => 'EUR', - 'decimal_separator' => ':', - 'decimals' => 1, - 'format' => '%s - %s', - 'thousand_separator' => '.', - 'trim_zeros' => 'no', - ); - } ); - $this->assertEquals( '<span class="lifterlms-price"><span class="llms-price-currency-symbol">€</span> - 1.003:0</span>', llms_price( '1002.999', $args ) ); - } - - /** - * test the llms_price_raw() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_llms_price_raw() { - - // test default case - $this->assertEquals( '$2.99', llms_price_raw( 2.99 ) ); - - // test with $args - $args = array( - 'currency' => 'GBP', - 'decimal_separator' => '.', - 'decimals' => 2, - 'format' => '<div>%s</div>%s', - 'thousand_separator' => ',', - 'trim_zeros' => 'no', - ); - $this->assertEquals( '£1,003.00', llms_price_raw( 1002.999, $args ) ); - } - - /** - * test the llms_trim_zeros() function - * - * @since 3.24.1 - * - * @return void - */ - public function test_llms_trim_zeros() { - - $this->assertEquals( '2', llms_trim_zeros( '2.00' ) ); - } -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-forms.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-forms.php deleted file mode 100644 index 1105e21d96..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-forms.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Test form-related functions - * - * @package LifterLMS/Tests - * - * @group form_functions - * @group forms - * @group functions - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Functions_Forms extends LLMS_UnitTestCase { - - /** - * Test llms_get_form() function. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_form() { - - $this->assertFalse( llms_get_form( 'fake' ) ); - $this->assertFalse( llms_get_form( 'checkout' ) ); - - LLMS_Forms::instance()->create( 'checkout' ); - $this->assertTrue( is_a( llms_get_form( 'checkout' ), 'WP_Post' ) ); - - } - - /** - * Test llms_get_form_html() function. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_form_html() { - - $this->assertEquals( '', llms_get_form_html( 'fake' ) ); - $this->assertEquals( '', llms_get_form_html( 'checkout' ) ); - - LLMS_Forms::instance()->create( 'checkout' ); - $this->assertTrue( '' !== llms_get_form_html( 'checkout' ) ); - - } - - /** - * test llms_get_form_title() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_form_title() { - - $this->assertEquals( '', llms_get_form_title( 'fake' ) ); - $this->assertEquals( '', llms_get_form_title( 'checkout' ) ); - - // Title enabled. - LLMS_Forms::instance()->create( 'checkout' ); - $this->assertEquals( 'Billing Information', llms_get_form_title( 'checkout' ) ); - - // Title disabled. - LLMS_Forms::instance()->create( 'account' ); - $this->assertEquals( '', llms_get_form_title( 'account' ) ); - - } - - /** - * Test llms_get_login_form() for a logged out user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_login_form_logged_out_user() { - - $res = $this->get_output( 'llms_get_login_form' ); - $this->assertStringContains( '<div class="llms-person-login-form-wrapper">', $res ); - $this->assertStringContains( '<form action="" class="llms-login" method="POST">', $res ); - - } - - /** - * Test llms_get_login_form() for a logged in user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_login_form_logged_in_user() { - - wp_set_current_user( $this->factory->user->create() ); - $this->assertOutputEmpty( 'llms_get_login_form' ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-l10n.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-l10n.php deleted file mode 100644 index 6f545a1939..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-l10n.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -/** - * Test Localization functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_l10n - * - * @since 4.9.0 - */ -class LLMS_Test_Functions_L10n extends LLMS_UnitTestCase { - - /** - * Test llms_get_locale() - * - * @since 4.9.0 - * - * @return void - */ - public function test_llms_get_locale() { - $this->assertEquals( 'en_US', llms_get_locale() ); - } - - /** - * Test llms_load_textdomain() as it would be used by a 3rd party. - * - * @since 4.9.0 - * - * @see LLMS_Test_Main_Class::test_localize() for coverage with default args against the LifterLMS core plugin. - * - * @return void - */ - public function test_llms_load_textdomain() { - - $dirs = array( - WP_LANG_DIR . '/lifterlms', // "Safe" directory. - WP_LANG_DIR . '/plugins', // Default language directory. - WP_PLUGIN_DIR . '/lifterlms-test/i18n', // Plugin language directory. - ); - - foreach ( $dirs as $dir ) { - - // Make sure the initial strings work. - $this->assertEquals( 'LifterLMS', __( 'LifterLMS', 'lifterlms-test' ), $dir ); - $this->assertEquals( 'Course', __( 'Course', 'lifterlms-test' ), $dir ); - - // Load a language file. - $file = LLMS_Unit_Test_Files::copy_asset( 'lifterlms-en_US.mo', $dir, 'lifterlms-test-en_US.mo' ); - llms_load_textdomain( 'lifterlms-test', WP_PLUGIN_DIR . '/lifterlms-test', 'i18n' ); - - $this->assertEquals( 'BetterLMS', __( 'LifterLMS', 'lifterlms-test' ), $dir ); - $this->assertEquals( 'Module', __( 'Course', 'lifterlms-test' ), $dir ); - - // Clean up. - LLMS_Unit_Test_Files::remove( $file ); - unload_textdomain( 'lifterlms-test' ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-locale.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-locale.php deleted file mode 100644 index 6af358b80c..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-locale.php +++ /dev/null @@ -1,243 +0,0 @@ -<?php -/** - * Tests for LifterLMS Locale functiosn - * - * @group functions - * @group locale - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Functions_Locale extends LLMS_UnitTestCase { - - /** - * Test the get_lifterlms_countries() method. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_lifterlms_countries() { - - $countries = get_lifterlms_countries(); - $this->assertTrue( is_array( get_lifterlms_countries() ) ); - - // Spot check presence of countries. - $this->assertEquals( 'United States', $countries['US'] ); - $this->assertEquals( 'United Kingdom', $countries['GB'] ); - $this->assertEquals( 'Australia', $countries['AU'] ); - $this->assertEquals( 'China', $countries['CN'] ); - $this->assertEquals( 'Afghanistan', $countries['AF'] ); - $this->assertEquals( 'Haiti', $countries['HT'] ); - $this->assertEquals( 'Nigeria', $countries['NG'] ); - $this->assertEquals( 'Slovakia', $countries['SK'] ); - $this->assertEquals( 'Uzbekistan', $countries['UZ'] ); - $this->assertEquals( 'Zimbabwe', $countries['ZW'] ); - - } - - /** - * test the get_lifterlms_country() function - * - * @since 3.24.1 - * @since 5.0.0 Moved from currency tests file. - * - * @return void - */ - public function test_get_lifterlms_country() { - - // test default - $this->assertEquals( 'US', get_lifterlms_country() ); - - // test lifterlms_country option - update_option( 'lifterlms_country', 'GB' ); - $this->assertEquals( 'GB', get_lifterlms_country() ); - - // test that the lifterlms_country filter is applied - add_filter( 'lifterlms_country', function() { - return 'FR'; - } ); - $this->assertEquals( 'FR', get_lifterlms_country() ); - } - - /** - * Test the llms_get_country_locale() function - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_country_address_info() { - - $this->assertEquals( array( - 'city' => 'City', - 'state' => 'State', - 'postcode' => 'ZIP code', - ), llms_get_country_address_info( 'US' ) ); - - $this->assertEquals( array(), llms_get_country_address_info( 'FAKE' ) ); - - } - - /** - * test the llms_get_country_name() function - * - * @since 3.24.1 - * @since 3.28.2 Unknown. - * @since 5.0.0 Moved from currency tests file. - * - * @return void - */ - public function test_llms_get_country_name() { - - // test existing country definition - $this->assertEquals( 'United States', llms_get_country_name( 'US' ) ); - - // test non-existing country definition - $this->assertEquals( 'XX', llms_get_country_name( 'XX' ) ); - } - - /** - * Test llms_get_time_period_l10n() - * - * @since 5.3.0 - * - * @return void - */ - public function test_llms_get_time_period_l10n() { - - /** - * List of tests to run - * - * Each array contains two items: - * 1) An array of arguments to pass to the function - * 2) the expected string output. - */ - $tests = array( - array( - array( 'day' ), - 'day', - ), - array( - array( 'dAy' ), - 'day', - ), - array( - array( 'day', 1 ), - 'day', - ), - array( - array( 'day', 2 ), - 'days', - ), - array( - array( 'day', 100 ), - 'days', - ), - array( - array( 'week' ), - 'week', - ), - array( - array( 'WEEK' ), - 'week', - ), - array( - array( 'week', 1 ), - 'week', - ), - array( - array( 'week', 2 ), - 'weeks', - ), - array( - array( 'week', 25 ), - 'weeks', - ), - array( - array( 'month' ), - 'month', - ), - array( - array( 'Month' ), - 'month', - ), - array( - array( 'month', 1 ), - 'month', - ), - array( - array( 'month', 2 ), - 'months', - ), - array( - array( 'month', 17 ), - 'months', - ), - array( - array( 'year' ), - 'year', - ), - array( - array( 'yeAR' ), - 'year', - ), - array( - array( 'year', 1 ), - 'year', - ), - array( - array( 'year', 2 ), - 'years', - ), - array( - array( 'year', 999 ), - 'years', - ), - array( - array( 'UNSUPPORTED' ), - 'UNSUPPORTED', - ), - ); - - foreach ( $tests as $test ) { - list( $args, $expect ) = $test; - $this->assertEquals( $expect, llms_get_time_period_l10n( ...$args ) ); - } - - } - - /** - * test the get_lifterlms_countries() function - * - * @since 3.24.1 - * @since 5.0.0 Updated name when adding test for the base function - * - * @return void - */ - public function test_get_lifterlms_countries_filter_and_unique() { - - // test unique and lifterlms_countries filters are applied - add_filter( 'lifterlms_countries', function() { - return array( - 'AF' => 'Afghanistan', - 'AL' => 'Albania', - 'DZ' => 'Algeria', - 'AS' => 'American Samoa', - 'AD' => 'Andorra', - 'AN' => 'Andorra', - ); - } ); - - $test = array( - 'AF' => 'Afghanistan', - 'AL' => 'Albania', - 'DZ' => 'Algeria', - 'AS' => 'American Samoa', - 'AD' => 'Andorra', - ); - - $this->assertEquals( $test, get_lifterlms_countries() ); - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-logs.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-logs.php deleted file mode 100644 index 45f832dc69..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-logs.php +++ /dev/null @@ -1,318 +0,0 @@ -<?php -/** - * Test Logging Functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_logs - * - * @since 4.5.0 - */ -class LLMS_Test_Functions_Logs extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 4.5.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - add_filter( 'llms_log_max_filesize', array( $this, 'shrink_max_log_size' ) ); - } - - /** - * Teardown - * - * Clean log files from the log directory. - * - * This isn't strictly necessary when running tests in a CI but if you run tests - * locally without regular manual cleanup you'll see a lot of trash logs generated as a result - * and this teardown prevents that. - * - * @since 4.5.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - foreach ( glob( LLMS_LOG_DIR . '*.log*' ) as $file ) { - unlink( $file ); - } - - remove_filter( 'llms_log_max_filesize', array( $this, 'shrink_max_log_size' ) ); - - } - - /** - * Create a mock log file with a target size - * - * @since 4.5.0 - * - * @param string $handle Log file's handle. - * @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. - * @return void - */ - protected function create_mock_log_file( $handle, $target_size = 1 ) { - - // Convert target size to MB. - $target_size = 1 * 1000 * 1000; - $file = llms_get_log_path( $handle ); - - $size = 0; - while ( $size < $target_size ) { - - $i = 0; - while ( $i <= 20 ) { - llms_log( str_repeat( '01', 999 ), $handle ); - ++$i; - } - - clearstatcache( true, $file ); - $size = filesize( $file ); - } - - } - - /** - * Mock the max allowed file size to be 1MB (instead of default 5MB) - * - * @since 4.5.0 - * - * @param int $size Default max file size. - * @return int - */ - public function shrink_max_log_size( $size ) { - return 1; - } - - /** - * Test llms_get_callable_name() - * - * @since 5.2.0 - * - * @return void - */ - public function test_llms_get_callable_name() { - - $tests = array( - array( - 'llms', - 'llms', - ), - array( - 'LLMS_Install::install', - 'LLMS_Install::install', - ), - array( - array( llms(), 'init' ), - 'LifterLMS->init', - ), - array( - array( 'LLMS_Install', 'install' ), - 'LLMS_Install::install', - ), - array( - llms(), - 'LifterLMS', - ), - array( - function() {}, - 'Closure' - ), - array( - array(), - 'Unknown', - ), - ); - - foreach ( $tests as $test ) { - - $callable = $test[0]; - $expected = $test[1]; - - $this->assertEquals( $expected, llms_get_callable_name( $callable ), $expected ); - - } - - } - - /** - * Test llms_get_log_path() - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_get_log_path() { - - $handle = 'testhandle'; - $expected_hash = wp_hash( $handle ); - - $expected_file = sprintf( '%1$s-%2$s.log', $handle, $expected_hash ); - - $path = llms_get_log_path( $handle ); - - $this->assertEquals( $expected_file, basename( $path ) ); - $this->assertEquals( untrailingslashit( LLMS_LOG_DIR ), dirname( $path ) ); - - $this->assertEquals( LLMS_LOG_DIR . $expected_file, $path ); - - } - - /** - * Test llms_log() when logging a string - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_log_string() { - - $this->assertTrue( llms_log( 'Test message', 'teststringlog' ) ); - - $logs = explode( ' - ', file_get_contents( llms_get_log_path( 'teststringlog' ) ) ); - - $this->assertTrue( date_create( $logs[0] ) instanceof DateTime ); - - $this->assertEquals( "Test message\n", $logs[1] ); - - } - - /** - * Test llms_log() when logging an array - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_log_array() { - - $this->assertTrue( llms_log( array( 'Test message' ), 'testarrlog' ) ); - - $logs = explode( ' - ', file_get_contents( llms_get_log_path( 'testarrlog' ) ) ); - - $this->assertTrue( date_create( $logs[0] ) instanceof DateTime ); - - $this->assertEquals( "Array -( - [0] => Test message -) - -", $logs[1] ); - - } - - /** - * Test llms_log() when logging an object - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_log_object() { - - $this->assertTrue( llms_log( (object) array( 'Test' => 1 ), 'testobjlog' ) ); - - $logs = explode( ' - ', file_get_contents( llms_get_log_path( 'testobjlog' ) ) ); - - $this->assertTrue( date_create( $logs[0] ) instanceof DateTime ); - - $this->assertEquals( "stdClass Object -( - [Test] => 1 -) - -", $logs[1] ); - - } - - /** - * Test llms_backup_log - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_backup_log() { - - $actions = did_action( 'llms_log_file_backup_created' ); - - $handle = 'logtobackup'; - $file = llms_get_log_path( $handle ); - - // File doesn't exist, no need to backup. - $this->assertNull( llms_backup_log( $handle ) ); - - llms_log( str_repeat( '01', 999 ), $handle ); - - // File does exist but doesn't need to be backup yet. - $this->assertNull( llms_backup_log( $handle ) ); - - $this->create_mock_log_file( $handle ); - - // Get the contents of the original to compare later. - $original = file_get_contents( $file ); - - // Split the file. - $copy = llms_backup_log( $handle ); - - // We made a copy. - $this->assertTrue( false !== $copy ); - - // Return should be different than than the original. - $this->assertNotEquals( $copy, $file ); - - // Copy exists. - $this->assertTrue( file_exists( $copy ) ); - - // Original has been removed. - $this->assertFalse( file_exists( $file ) ); - - // Compare copy contents to the original. - $this->assertEquals( $original, file_get_contents( $copy ) ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'llms_log_file_backup_created' ) ); - - } - - /** - * Test llms_backup_logs() - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_backup_logs() { - - $actions = did_action( 'llms_log_file_backup_created' ); - - // Make sure the created files are the right ones. - $handler = function( $copy, $file, $handle ) { - $this->assertTrue( in_array( $handle, array( 'tobackup1', 'tobackup2', 'tobackup-withonehyphen', 'tobackup-with-mutli-hyphens' ), true ) ); - }; - add_action( 'llms_log_file_backup_created', $handler, 10, 3 ); - - llms_log( 'message', 'notbackedup1' ); - llms_log( 'message', 'notbackedup2' ); - - $this->create_mock_log_file( 'tobackup1' ); - $this->create_mock_log_file( 'tobackup2' ); - $this->create_mock_log_file( 'tobackup-withonehyphen' ); - $this->create_mock_log_file( 'tobackup-with-mutli-hyphens' ); - - llms_backup_logs(); - - $this->assertEquals( $actions + 4, did_action( 'llms_log_file_backup_created' ) ); - - remove_action( 'llms_log_file_backup_created', $handler, 10 ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-options.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-options.php deleted file mode 100644 index 8b2da594a9..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-options.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * Test Option functions - * - * @package LifterLMS/Tests/Functions - * @since 3.29.0 - * @version 3.29.0 - */ -class LLMS_Test_Functions_Options extends LLMS_UnitTestCase { - - /** - * test the get_secure_var method - * - * @return void - * @since 3.29.0 - * @version 3.29.0 - */ - public function test_llms_get_secure_option() { - - $val = 'F4K3_ApI-K3Y$!'; - - // nothing set. - $this->assertFalse( llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR' ) ); - // fallback to something else. - $this->assertEquals( '', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', '' ) ); - // fallback to actual val. - $this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', $val ) ); - // fallback with db call. - $this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', $val, 'llms_mock_secure_option' ) ); - // no fallback with db call. - $this->assertFalse( llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) ); - - // add the option. - update_option( 'llms_mock_secure_option', $val ); - $this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) ); - - // use constant variable. - define( 'LLMS_MOCK_SECURE_VAR', 'arstarstarst' ); - $this->assertEquals( 'arstarstarst', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) ); - - // use environment var. - putenv( 'LLMS_MOCK_SECURE_VAR=a90rst0-98arst' ); - $this->assertEquals( 'a90rst0-98arst', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-order.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-order.php deleted file mode 100644 index ee77caa88c..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-order.php +++ /dev/null @@ -1,369 +0,0 @@ -<?php -/** - * Test Order Functions - * - * @package LifterLMS/Tests/Functions - * - * @group orders - * - * @group orders - * @group functions - * @group functions_orders - * - * @since 3.27.0 - * @since 5.0.0 Updated for form handler error codes & install forms on setup. - * @since 5.4.0 Added tests for `llms_get_possible_order_statuses()`. - */ -class LLMS_Test_Functions_Order extends LLMS_UnitTestCase { - - /** - * Test the llms_get_order_by_key() method. - * - * @since 3.30.1 - * - * @return void - */ - public function test_llms_get_order_by_key() { - - // Errors. - $this->assertTrue( is_null( llms_get_order_by_key( 'arst' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'order' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'id' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'fake' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '1' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '1', 'order' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '1', 'id' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '1', 'fake' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 12345 ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 12345, 'order' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 12345, 'id' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( 12345, 'fake' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '', 'order' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '', 'id' ) ) ); - $this->assertTrue( is_null( llms_get_order_by_key( '', 'fake' ) ) ); - - // Success. - $order = new LLMS_Order( 'new' ); - $this->assertEquals( $order, llms_get_order_by_key( $order->get( 'order_key' ) ) ); // Default. - $this->assertEquals( $order, llms_get_order_by_key( $order->get( 'order_key' ), 'order' ) ); // Explicit. - $this->assertEquals( $order->get( 'id' ), llms_get_order_by_key( $order->get( 'order_key' ), 'id' ) ); // Id. - $this->assertEquals( $order->get( 'id' ), llms_get_order_by_key( $order->get( 'order_key' ), 'somethingelse' ) ); // Fake. - - } - - /** - * Test llms_get_order_status_name(). - * - * @since 3.3.1 - * - * @return void - */ - public function test_llms_get_order_status_name() { - $this->assertNotEmpty( llms_get_order_status_name( 'llms-active' ) ); - $this->assertEquals( 'Active', llms_get_order_status_name( 'llms-active' ) ); - $this->assertEquals( 'wut', llms_get_order_status_name( 'wut' ) ); - } - - /** - * Test llms_get_order_statuses(). - * - * @since 3.3.1 - * @since 3.19.0 Unknown. - * - * @return void - */ - public function test_llms_get_order_statuses() { - - $this->assertTrue( is_array( llms_get_order_statuses() ) ); - $this->assertFalse( empty( llms_get_order_statuses() ) ); - $this->assertEquals( array( - 'llms-completed', - 'llms-active', - 'llms-expired', - 'llms-on-hold', - 'llms-pending-cancel', - 'llms-pending', - 'llms-cancelled', - 'llms-refunded', - 'llms-failed', - ), array_keys( llms_get_order_statuses() ) ); - - $this->assertTrue( is_array( llms_get_order_statuses( 'recurring' ) ) ); - $this->assertFalse( empty( llms_get_order_statuses( 'recurring' ) ) ); - $this->assertEquals( array( - 'llms-active', - 'llms-expired', - 'llms-on-hold', - 'llms-pending-cancel', - 'llms-pending', - 'llms-cancelled', - 'llms-refunded', - 'llms-failed', - ), array_keys( llms_get_order_statuses( 'recurring' ) ) ); - - $this->assertTrue( is_array( llms_get_order_statuses( 'single' ) ) ); - $this->assertFalse( empty( llms_get_order_statuses( 'single' ) ) ); - $this->assertEquals( array( - 'llms-completed', - 'llms-pending', - 'llms-cancelled', - 'llms-refunded', - 'llms-failed', - ), array_keys( llms_get_order_statuses( 'single' ) ) ); - - } - - /** - * Test llms_locate_order_for_user_and_plan() method. - * - * @since 3.30.1 - * - * @return void - */ - public function test_llms_locate_order_for_user_and_plan() { - - $order = new LLMS_Order( 'new' ); - - $uid = $this->factory->student->create(); - $pid = $this->factory->post->create( array( - 'post_type' => 'llms_access_plan', - ) ); - - // Fake student & fake plan - $this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid + 1 ) ) ); - - // Real student & fake plan - $this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid, $pid + 1 ) ) ); - - // Fake student & real plan - $this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid ) ) ); - - // Real student & real plan & no order exists. - $this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid ) ) ); - - // Real student & real plan & order exists. - $order->set( 'user_id', $uid ); - $order->set( 'plan_id', $pid ); - $this->assertSame( $order->get( 'id' ), llms_locate_order_for_user_and_plan( $uid, $pid ) ); - - } - - /** - * Test llms_setup_pending_order() - * - * @since 3.27.0 - * @since 5.0.0 Install forms & Updated expected error code. - * Only logged in users can edit themselves. - * @return void - */ - public function test_llms_setup_pending_order() { - - LLMS_Forms::instance()->install( true ); - - // Enable t&c. - update_option( 'lifterlms_registration_require_agree_to_terms', 'yes' ); - update_option( 'lifterlms_terms_page_id', 123456789 ); - - // Order data to pass to tests. - // Will be built upon as we go through tests below. - $order_data = array( - 'plan_id' => '', - 'agree_to_terms' => '', - 'payment_gateway' => '', - 'coupon_code' => '', - 'customer' => array(), - ); - - // Didn't agree to t&c. - $this->setup_pending_order_fail( $order_data, 'terms-violation' ); - - // Agree to t&c for all future tests. - $order_data['agree_to_terms'] = 'yes'; - - // Missing plan id. - $this->setup_pending_order_fail( $order_data, 'missing-plan-id' ); - - // Add a fake plan id. - $order_data['plan_id'] = 123; - $this->setup_pending_order_fail( $order_data, 'invalid-plan-id' ); - - // Create a real plan and add it to the order data. - $order_data['plan_id'] = $this->factory->post->create( array( - 'post_type' => 'llms_access_plan', - 'post_title' => 'plan name', - ) ); - update_post_meta( $order_data['plan_id'], '_llms_price', '25.00' ); - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - update_post_meta( $order_data['plan_id'], '_llms_product_id', $course_id ); - - // Fake coupon code. - $order_data['coupon_code'] = 'coupon'; - $this->setup_pending_order_fail( $order_data, 'coupon-not-found' ); - - // Create a real coupon. - $coupon_id = $this->factory->post->create( array( - 'post_type' => 'llms_coupon', - 'post_title' => 'coupon', - ) ); - // But make it unusable. - update_post_meta( $coupon_id, '_llms_expiration_date', date( 'm/d/Y', strtotime( '-1 year' ) ) ); - $this->setup_pending_order_fail( $order_data, 'invalid-coupon' ); - - // Make the coupon usable. - update_post_meta( $coupon_id, '_llms_expiration_date', date( 'm/d/Y', strtotime( '+5 years' ) ) ); - - // Missing payment gateway. - $this->setup_pending_order_fail( $order_data, 'missing-gateway-id' ); - - // Fake payment gateway. - $order_data['payment_gateway'] = 'fakeway'; - $this->setup_pending_order_fail( $order_data, 'invalid-gateway' ); - - // Real payment gateway. - $order_data['payment_gateway'] = 'manual'; - - // No customer data. - $this->setup_pending_order_fail( $order_data, 'missing-customer' ); - - // Most customer data but missing required email confirm field. - $order_data['customer'] = array( - 'user_login' => 'arstehnarst', - 'email_address' => 'arstinhasrteinharst@test.net', - 'password' => '12345678', - 'password_confirm' => '12345678', - 'first_name' => 'Test', - 'last_name' => 'Person', - 'llms_billing_address_1' => '123', - 'llms_billing_address_2' => '123', - 'llms_billing_city' => 'City', - 'llms_billing_state' => 'CA', - 'llms_billing_zip' => '91231', - 'llms_billing_country' => 'US', - 'llms_phone' => '1234567890', - ); - - // Missing required field. - $this->setup_pending_order_fail( $order_data, 'llms-form-missing-required' ); - - // Existing user who's already enrolled. - $uid = $this->factory->user->create( array( 'role' => 'student' ) ); - wp_set_current_user( $uid ); - $order_data['customer']['email_address_confirm'] = 'arstinhasrteinharst@test.net'; - $order_data['customer']['user_id'] = $uid; - llms_enroll_student( $uid, $course_id ); - $this->setup_pending_order_fail( $order_data, 'already-enrolled' ); - - // This should return an array of details we need to create a new order! - unset( $order_data['customer']['user_id'] ); - wp_set_current_user( null ); - $order_data['customer']['email_address'] = 'arstarst@ats.net'; - $order_data['customer']['email_address_confirm'] = 'arstarst@ats.net'; - $setup = llms_setup_pending_order( $order_data ); - $this->assertEquals( array( 'person', 'plan', 'gateway', 'coupon' ), array_keys( $setup ) ); - - } - - /** - * Test llms_get_possible_order_statuses() function for a recurring order. - * - * @since 5.4.0 - * - * @return void - */ - public function test_get_possible_recurring_order_statuses() { - $order = $this->get_mock_order(); - $this->assertTrue( $order->is_recurring() ); - $this->assertEquals( - llms_get_order_statuses( 'recurring' ), - llms_get_possible_order_statuses( $order ) - ); - } - - /** - * Test llms_get_possible_order_statuses() function for a single order. - * - * @since 5.4.0 - * - * @return void - */ - public function test_get_possible_single_order_statuses() { - $order = $this->get_mock_order(); - $order->set( 'order_type', 'single' ); - $this->assertFalse( $order->is_recurring() ); - $this->assertEquals( - llms_get_order_statuses( 'single' ), - llms_get_possible_order_statuses( $order ) - ); - } - - /** - * Test llms_get_possible_order_statuses() function for a recurring order with deleted product. - * - * @since 5.4.0 - * - * @return void - */ - public function test_get_possible_recurring_order_statuses_deleted_product() { - - $order = $this->get_mock_order(); - - // Delete product. - wp_delete_post( $order->get( 'product_id' ) ); - - $this->assertTrue( $order->is_recurring() ); - $this->assertEquals( - array( - 'llms-expired', - 'llms-cancelled', - 'llms-refunded', - 'llms-failed', - ), - array_keys( llms_get_possible_order_statuses( $order ) ) - ); - - } - - /** - * Test llms_get_possible_order_statuses() function for a single with deleted product. - * - * @since 5.4.0 - * - * @return void - */ - public function test_get_possible_single_order_statuses_deleted_product() { - - $order = $this->get_mock_order(); - $order->set( 'order_type', 'single' ); - - // Delete product. - wp_delete_post( $order->get( 'product_id' ) ); - - $this->assertFalse( $order->is_recurring() ); - $this->assertEquals( - llms_get_order_statuses( 'single' ), - llms_get_possible_order_statuses( $order ) - ); - - } - - - /** - * Test llms_setup_pending_order() failure - * - * @since 3.27.0 - * @since 4.9.0 Remove default optional value from `$order_data` arg for php8 compat. - * - * @param array $order_data Array of order data to pass to `llms_setup_pending_order()`. - * @param string $expected_code Expected error code. - * @return void - */ - private function setup_pending_order_fail( $order_data, $expected_code ) { - - $setup = llms_setup_pending_order( $order_data ); - $this->assertTrue( is_wp_error( $setup ) ); - $this->assertEquals( $expected_code, $setup->get_error_code() ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-page.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-page.php deleted file mode 100644 index 886380c8fd..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-page.php +++ /dev/null @@ -1,196 +0,0 @@ -<?php -/** - * Test page functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_page - * - * @since 3.38.0 - */ -class LLMS_Test_Functions_Fage extends LLMS_UnitTestCase { - - /** - * Test the llms_confirm_payment_url() function. - * - * @since 3.38.0 - * - * @return void - */ - public function test_llms_confirm_payment_url() { - - LLMS_Install::create_pages(); - - $base = get_permalink( llms_get_page_id( 'checkout' ) ) . '&confirm-payment'; - - // No additional args provided. - $this->assertEquals( $base, llms_confirm_payment_url() ); - - // Has order key. - $this->assertEquals( $base . '&order=fake', llms_confirm_payment_url( 'fake' ) ); - - // Has redirect. - $this->mockGetRequest( array( - 'redirect' => get_site_url(), - ) ); - $this->assertEquals( $base . '&redirect=' . urlencode( get_site_url() ), llms_confirm_payment_url() ); - - // Has both. - $this->assertEquals( $base . '&order=fake&redirect=' . urlencode( get_site_url() ), llms_confirm_payment_url( 'fake' ) ); - - } - - /** - * Test llms_get_endpoint_url() when pretty permalinks are disabled. - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_get_endpoint_url_no_pretty_permalinks() { - - LLMS_Install::create_pages(); - - $permalink = get_permalink( llms_get_page_id( 'myaccount' ) ); - $this->go_to( $permalink ); - - foreach ( llms()->query->get_query_vars() as $var => $slug ) { - - $this->assertEquals( "{$permalink}&{$slug}", llms_get_endpoint_url( $var ) ); - $this->assertEquals( "{$permalink}&{$slug}=test", llms_get_endpoint_url( $var, 'test' ) ); - $this->assertEquals( "https://fake.tld/?{$slug}=1", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) ); - - } - - } - - /** - * Test llms_get_endpoint_url() when pretty permalinks are enabled with a trailing slash. - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_get_endpoint_url_with_trailing_slash_pretty_permalink() { - - global $wp_rewrite; - - $orig_permastruct = get_option( 'permalink_structure' ); - - LLMS_Install::create_pages(); - - update_option( 'permalink_structure', '/%postname%/' ); - $wp_rewrite->init(); - - - $permalink = get_permalink( llms_get_page_id( 'myaccount' ) ); - $this->go_to( $permalink ); - - foreach ( llms()->query->get_query_vars() as $var => $slug ) { - - $this->assertEquals( "{$permalink}{$slug}/", llms_get_endpoint_url( $var ) ); - $this->assertEquals( "{$permalink}{$slug}/test/", llms_get_endpoint_url( $var, 'test' ) ); - $this->assertEquals( "https://fake.tld/{$slug}/1/", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) ); - $this->assertEquals( "https://fake.tld/{$slug}/1/?whatever=yes", llms_get_endpoint_url( $var, 1, 'https://fake.tld/?whatever=yes' ) ); - - } - - $this->go_to( '' ); - - update_option( 'permalink_structure', $orig_permastruct ); - $wp_rewrite->init(); - - } - - /** - * Test llms_get_endpoint_url() when pretty permalinks are enabled with a trailing slash. - * - * @since 5.9.0 - * - * @link https://github.com/gocodebox/lifterlms/issues/1983 - * - * @return void - */ - public function test_llms_get_endpoint_url_without_trailing_slash_pretty_permalink() { - - global $wp_rewrite; - - $orig_permastruct = get_option( 'permalink_structure' ); - - LLMS_Install::create_pages(); - - update_option( 'permalink_structure', '/%postname%' ); - $wp_rewrite->init(); - - - $permalink = get_permalink( llms_get_page_id( 'myaccount' ) ); - $this->go_to( $permalink ); - - foreach ( llms()->query->get_query_vars() as $var => $slug ) { - - $this->assertEquals( "{$permalink}/{$slug}", llms_get_endpoint_url( $var ) ); - $this->assertEquals( "{$permalink}/{$slug}/test", llms_get_endpoint_url( $var, 'test' ) ); - $this->assertEquals( "https://fake.tld/{$slug}/1", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) ); - $this->assertEquals( "https://fake.tld/{$slug}/1?whatever=yes", llms_get_endpoint_url( $var, 1, 'https://fake.tld/?whatever=yes' ) ); - - } - - $this->go_to( '' ); - - update_option( 'permalink_structure', $orig_permastruct ); - $wp_rewrite->init(); - - } - - /** - * Test the llms_get_page_id() function. - * - * @since 3.38.0 - * - * @return void - */ - public function test_llms_get_page_id() { - - $pages = array( - 'checkout' => 'checkout', - 'courses' => 'shop', - 'myaccount' => 'myaccount', - 'memberships' => 'memberships', - ); - - // Clear options maybe installed by other tests. - foreach ( array_values( $pages ) as $option ) { - delete_option( 'lifterlms_' . $option . '_page_id' ); - } - - // Options don't exist. - - // Backwards compat. - $this->assertEquals( -1, llms_get_page_id( 'shop' ) ); - - foreach ( array_keys( $pages ) as $slug ) { - $this->assertEquals( -1, llms_get_page_id( $slug ) ); - } - - // Options do exist. - LLMS_Install::create_pages(); - - // Backwards compat. - $this->assertEquals( get_option( 'lifterlms_shop_page_id' ), llms_get_page_id( 'shop' ) ); - - foreach ( $pages as $slug => $option ) { - - $id = llms_get_page_id( $slug ); - - // Number. - $this->assertTrue( is_int( $id ) ); - - // Equals expected option value. - $this->assertEquals( get_option( 'lifterlms_' . $option . '_page_id' ), $id ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-person.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-person.php deleted file mode 100644 index 007e12017b..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-person.php +++ /dev/null @@ -1,389 +0,0 @@ -<?php -/** - * Tests for LifterLMS Core Functions - * - * @group functions_person - * @group functions - * @group LLMS_Student - * - * @since 3.8.0 - * @since 3.9.0 Add tests for `llms_get_student()`. - * @since 3.9.0 Add tests for `llms_get_usernames_blacklist()`. - * @since 5.0.0 Add tests for `llms_set_password_reset_cookie()` and `llms_parse_password_reset_cookie()`. - */ -class LLMS_Test_Functions_Person extends LLMS_UnitTestCase { - - /** - * Test llms_can_user_bypass_restrictions() - * - * @since 3.8.0 - * - * @return void - */ - public function test_llms_can_user_bypass_restrictions() { - - // Allow admins to bypass. - update_option( 'llms_grant_site_access', array( 'administrator' ) ); - - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - $instructor = $this->factory->user->create( array( 'role' => 'instructor' ) ); - $student = $this->factory->user->create( array( 'role' => 'student' ) ); - - $this->assertTrue( llms_can_user_bypass_restrictions( $admin ) ); - $this->assertFalse( llms_can_user_bypass_restrictions( $student ) ); - - $this->assertFalse( llms_can_user_bypass_restrictions( 'fake' ) ); - - // Pass in a student. - $this->assertTrue( llms_can_user_bypass_restrictions( $admin ) ); - - // Should still work with two roles. - update_option( 'llms_grant_site_access', array( 'administrator', 'editor' ) ); - $this->assertTrue( llms_can_user_bypass_restrictions( $admin ) ); - - // Test restrictions against a post. - update_option( 'llms_grant_site_access', array( 'administrator', 'editor', 'instructor' ) ); - $course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) ); - $course = llms_get_post( $course_id ); - $lesson = $course->get_lessons()[0]; - $quiz = $lesson->get_quiz(); - $tests = array( $course_id, $lesson->get( 'id' ), $quiz->get( 'id' ) ); - - foreach ( $tests as $post_id ) { - - $this->assertTrue( llms_can_user_bypass_restrictions( $admin, $post_id ) ); - $this->assertFalse( llms_can_user_bypass_restrictions( $instructor, $post_id ) ); - $this->assertFalse( llms_can_user_bypass_restrictions( $student, $post_id ) ); - - } - - $course->set_instructors( array( - array( - 'id' => $instructor, - ) - ) ); - - foreach ( $tests as $post_id ) { - - $this->assertTrue( llms_can_user_bypass_restrictions( $admin, $post_id ) ); - $this->assertTrue( llms_can_user_bypass_restrictions( $instructor, $post_id ) ); - $this->assertFalse( llms_can_user_bypass_restrictions( $student, $post_id ) ); - - } - - } - - /** - * Test llms_get_minimum_password_strength_name(). - * - * @since Unknown. - * - * @return void - */ - public function test_llms_get_minimum_password_strength_name() { - - // Default value. - $this->assertEquals( 'strong', llms_get_minimum_password_strength_name() ); - - // Existing options. - $this->assertEquals( 'strong', llms_get_minimum_password_strength_name( 'strong' ) ); - $this->assertEquals( 'medium', llms_get_minimum_password_strength_name( 'medium' ) ); - $this->assertEquals( 'weak', llms_get_minimum_password_strength_name( 'weak' ) ); - $this->assertEquals( 'very weak', llms_get_minimum_password_strength_name( 'very-weak' ) ); - - // Custom option. - $this->assertEquals( 'fake', llms_get_minimum_password_strength_name( 'fake' ) ); - - } - - /** - * Test llms_get_student - * - * @since 3.9.0 - * - * @return void - */ - public function test_llms_get_student() { - - $uid = $this->factory->user->create(); - - $this->assertTrue( is_a( llms_get_student( $uid ), 'LLMS_Student' ) ); - $this->assertTrue( is_a( llms_get_student( new WP_User( $uid ) ), 'LLMS_Student' ) ); - $this->assertTrue( is_a( llms_get_student( new LLMS_Student( $uid ) ), 'LLMS_Student' ) ); - - $this->assertFalse( is_a( llms_get_student( $uid + 1 ), 'LLMS_Student' ) ); - $this->assertFalse( is_a( llms_get_student( 'string' ), 'LLMS_Student' ) ); - - } - - /** - * Test llms_get_usernames_blocklist() function. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_usernames_blocklist() { - - $this->assertTrue( is_array( llms_get_usernames_blocklist() ) ); - $this->assertTrue( in_array( 'admin', llms_get_usernames_blocklist(), true ) ); - - } - - /** - * Test llms_parse_password_reset_cookie() when no cookie is set. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_no_cookie() { - - $this->cookies->unset( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - - $res = llms_parse_password_reset_cookie(); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_no_cookie', $res ); - - } - - /** - * Test llms_parse_password_reset_cookie() when the cookie is malformed. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_bad_cookie() { - - llms_set_password_reset_cookie( 'fake' ); - - $res = llms_parse_password_reset_cookie(); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_cookie', $res ); - - } - - /** - * Test llms_parse_password_reset_cookie() when the user doesn't exist. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_bad_user() { - - $uid = $this->factory->user->create() + 1; // Fake user. - - llms_set_password_reset_cookie( sprintf( '%d:fake', $uid ) ); - - $res = llms_parse_password_reset_cookie(); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res ); - - } - - /** - * Test llms_parse_password_reset_cookie() when the key is invalid. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_bad_key() { - - $uid = $this->factory->user->create(); - - llms_set_password_reset_cookie( sprintf( '%d:fake', $uid ) ); - - $res = llms_parse_password_reset_cookie(); - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res ); - - } - - /** - * Test llms_parse_password_reset_cookie() when the key is expired. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_expired_key() { - - add_filter( 'password_reset_expiration', '__return_zero' ); - - $user = $this->factory->user->create_and_get(); - $key = get_password_reset_key( $user ); - - llms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) ); - - $res = llms_parse_password_reset_cookie(); - - $this->assertWPError( $res ); - $this->assertWPErrorCodeEquals( 'llms_password_reset_expired_key', $res ); - - remove_filter( 'password_reset_expiration', '__return_zero' ); - - } - - /** - * Test llms_parse_password_reset_cookie() success. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_parse_password_reset_cookie_success() { - - $user = $this->factory->user->create_and_get(); - $key = get_password_reset_key( $user ); - - llms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) ); - - $res = llms_parse_password_reset_cookie(); - - $this->assertEquals( $user->user_login, $res['login'] ); - $this->assertEquals( $key, $res['key'] ); - - } - - /** - * Test llms_set_password_reset_cookie() under default circumstances - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_set_password_reset_cookie() { - - $name = sprintf( 'wp-resetpass-%s', COOKIEHASH ); - $this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) ); - - $this->assertArrayHasKey( $name, $this->cookies->get_all() ); - $this->assertEquals( array( - 'value' => 'reset_pass', - 'expires' => 0, - 'path' => '', - 'domain' => COOKIE_DOMAIN, - 'secure' => false, - 'httponly' => true, - ), $this->cookies->get( $name ) ); - - } - - /** - * Test that the llms_set_password_reset_cookie fails. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_set_password_reset_cookie_fail() { - - // Mock failure. - $this->cookies->expect_error(); - - $this->assertFalse( llms_set_password_reset_cookie( 'cookieval' ) ); - - } - - /** - * Test llms_set_password_reset_cookie() when no value is set (expires the cookie). - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_set_password_reset_cookie_no_val() { - - $this->assertTrue( llms_set_password_reset_cookie() ); - $data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertEmpty( $data['value'] ); - $this->assertTrue( time() > $data['expires'] ); - - } - - /** - * Test llms_set_password_reset_cookie() sets the cookie path properly. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_set_password_reset_cookie_path() { - - // Regular URL path. - $_SERVER['REQUEST_URI'] = '/dashboard/lost-password'; - - $this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) ); - $data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertEquals( '/dashboard/lost-password', $data['path'] ); - - // With query string. - $_SERVER['REQUEST_URI'] = '/dashboard/lost-password?var1=1&var2=2'; - - $this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) ); - $data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertEquals( '/dashboard/lost-password', $data['path'] ); - - // Reset. - $_SERVER['REQUEST_URI'] = ''; - - } - - /** - * Test llms_set_password_reset_cookie() sets a secure cookie when SSL is enabled on the site. - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_set_password_reset_cookie_ssl() { - - // Mock is_ssl() to return `true`. - $_SERVER['HTTPS'] = 'ON'; - - $this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) ); - $data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ); - $this->assertTrue( $data['secure'] ); - - // Reset. - unset( $_SERVER['HTTPS'] ); - - } - - /** - * Test llms_set_person_auth_cookie() - * - * @since 4.5.0 - * - * @expectedDeprecated llms_set_person_auth_cookie - * - * @return void - */ - public function test_llms_set_person_auth_cookie() { - llms_set_person_auth_cookie( $this->factory->user->create() ); - } - - /** - * Test llms_set_user_login_time() - * - * @since 4.5.0 - * - * @return void - */ - public function test_llms_set_user_login_time() { - - $user = $this->factory->user->create_and_get(); - - $date = '2020-03-21 10:32:48'; - llms_tests_mock_current_time( $date ); - - llms_set_user_login_time( $user->user_login, $user ); - - $this->assertEquals( $date, get_user_meta( $user->ID, 'llms_last_login', true ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-progression.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-progression.php deleted file mode 100644 index 128a2b664f..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-progression.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * Test course and lesson progression functions. - * - * @group functions - * @group progression_functions - * @package LifterLMS/Tests/Functions - * @since 3.29.0 - * @version 3.29.0 - */ -class LLMS_Test_Functions_Progression extends LLMS_Unit_Test_Case { - - /** - * Test the llms_allow_lesson_completion() method. - * - * @return void - * @since 3.29.0 - * @version 3.29.0 - */ - public function test_llms_allow_lesson_completion() { - - $student = $this->factory->student->create_and_get(); - $course = $this->factory->course->create_and_get(); - $lesson_id = $course->get_lessons( 'ids' )[0]; - - // progression is okay with no intervention. - $this->assertTrue( llms_allow_lesson_completion( $student->get( 'id' ), $lesson_id ) ); - - // something somewhere prevents progression. - add_filter( 'llms_allow_lesson_completion', '__return_false' ); - $this->assertFalse( llms_allow_lesson_completion( $student->get( 'id' ), $lesson_id ) ); - - // remove the filter so we don't potentially break other tests. - remove_filter( 'llms_allow_lesson_completion', '__return_false' ); - - } - - /** - * Test the llms_show_mark_complete_button() method. - * - * @return void - * @since 3.29.0 - * @version 3.29.0 - */ - public function test_llms_show_mark_complete_button() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 3, 'quizzes' => 2 ) ); - $no_quiz = $course->get_lessons()[0]; - $has_quiz = $course->get_lessons()[1]; - - $has_unpublished_quiz = $course->get_lessons()[2]; - $has_unpublished_quiz->get_quiz()->set( 'status', 'draft' ); - - $this->assertTrue( llms_show_mark_complete_button( $no_quiz ) ); - $this->assertFalse( llms_show_mark_complete_button( $has_quiz ) ); - $this->assertTrue( llms_show_mark_complete_button( $has_unpublished_quiz ) ); - - } - - /** - * Test the llms_show_take_quiz_button() - * @return void - * @since 3.29.0 - * @version 3.29.0 - */ - public function test_llms_show_take_quiz_button() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 3, 'quizzes' => 2 ) ); - $no_quiz = $course->get_lessons()[0]; - $has_quiz = $course->get_lessons()[1]; - - $has_unpublished_quiz = $course->get_lessons()[2]; - $has_unpublished_quiz->get_quiz()->set( 'status', 'draft' ); - - $this->assertFalse( llms_show_take_quiz_button( $no_quiz ) ); - $this->assertTrue( llms_show_take_quiz_button( $has_quiz ) ); - $this->assertFalse( llms_show_take_quiz_button( $has_unpublished_quiz ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-template.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-template.php deleted file mode 100644 index 80cb23ad10..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-template.php +++ /dev/null @@ -1,318 +0,0 @@ -<?php -/** - * Test functions template - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_template - * - * @since 4.8.0 - * @since 5.9.0 Added tests on llms_template_file_path(). - */ -class LLMS_Test_Functions_Template extends LLMS_UnitTestCase { - - private $themes = array( - 'fake_parent', - 'fake_child', - ); - - /** - * Setup test cases. - * - * @since 4.8.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * @since 5.9.0 Clean theme overrides directories cache. - * - * @return void - */ - public function set_up() { - parent::set_up(); - foreach ( $this->themes as $theme ) { - $this->_delete_theme_override_directory( $theme ); - } - wp_cache_delete( 'theme-override-directories', 'llms_template_functions' ); - } - - /** - * Test llms_get_template_override_directories() when only parent theme override dir is present - * - * @since 4.8.0 - * - * @return void - */ - public function test_llms_get_template_override_directories_only_parent_theme() { - $original_template = get_option( 'template', '' ); - update_option( 'template', 'fake_parent' ); - $this->_create_theme_override_directory( 'fake_parent' ); - $template_directories = llms_get_template_override_directories(); - - $this->assertEquals( - array( - get_theme_root() . '/fake_parent/lifterlms' - ), - array_values( $template_directories ) - ); - - $this->_delete_theme_override_directory( 'fake_parent' ); - update_option( 'template', $original_template ); - } - - /** - * Test llms_get_template_override_directories() when parent and child theme override dir are present - * - * @since 4.8.0 - * - * @return void - */ - public function test_llms_get_template_override_directories_parent_and_child_theme() { - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake_parent' ); - update_option( 'stylesheet', 'fake_child' ); - $this->_create_theme_override_directory( 'fake_parent' ); - $this->_create_theme_override_directory( 'fake_child' ); - - $template_directories = llms_get_template_override_directories(); - - $this->assertEquals( - array( - get_theme_root() . '/fake_child/lifterlms', - get_theme_root() . '/fake_parent/lifterlms' - ), - $template_directories - ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_get_template_override_directories() when parent and child theme are present but only parent overrides - * - * @since 4.8.0 - * - * @return void - */ - public function test_llms_get_template_override_directories_parent_and_child_theme_parent_overrides() { - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake_parent' ); - update_option( 'stylesheet', 'fake_child' ); - $this->_create_theme_override_directory( 'fake_parent' ); - $this->_create_theme_override_directory( 'fake_child' ); - - rmdir( get_theme_root() . '/fake_child/lifterlms' ); - - $template_directories = llms_get_template_override_directories(); - - $this->assertEquals( - array( - get_theme_root() . '/fake_parent/lifterlms' - ), - array_values( $template_directories ) - ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_get_template_override_directories() when parent and child theme are present but only child overrides - * - * @since 4.8.0 - * - * @return void - */ - public function test_llms_get_template_override_directories_parent_and_child_theme_child_overrides() { - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake_parent' ); - update_option( 'stylesheet', 'fake_child' ); - $this->_create_theme_override_directory( 'fake_parent' ); - $this->_create_theme_override_directory( 'fake_child' ); - - rmdir( get_theme_root() . '/fake_parent/lifterlms' ); - - $template_directories = llms_get_template_override_directories(); - - $this->assertEquals( - array( - get_theme_root() . '/fake_child/lifterlms' - ), - array_values( $template_directories ) - ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_get_template_override_directories() when parent and child theme are present but none of them overrides - * - * @since 4.8.0 - * - * @return void - */ - public function test_llms_get_template_override_directories_parent_and_child_theme_no_override() { - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake_parent' ); - update_option( 'stylesheet', 'fake_child' ); - $this->_create_theme_override_directory( 'fake_parent' ); - $this->_create_theme_override_directory( 'fake_child' ); - - rmdir( get_theme_root() . '/fake_parent/lifterlms' ); - rmdir( get_theme_root() . '/fake_child/lifterlms' ); - - $template_directories = llms_get_template_override_directories(); - - $this->assertEmpty( $template_directories ); - - $this->_delete_theme_override_directory( 'fake_child' ); - $this->_delete_theme_override_directory( 'fake_parent' ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_template_file_path() passing an empty template file. - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_template_file_path_empty_template_file_passed() { - - $this->assertEquals( - llms()->plugin_path() . '/templates/', - llms_template_file_path( '' ) - ); - - /** - * Simulate the activation of a theme with the templates directory overridden. - */ - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - wp_cache_delete( 'theme-override-directories', 'llms_template_functions' ); - update_option( 'template', 'fake' ); - update_option( 'stylesheet', 'fake' ); - $this->_create_theme_override_directory( 'fake' ); - - $this->assertEquals( - get_theme_root() . '/fake/lifterlms/', - llms_template_file_path( '' ) - ); - - $this->_delete_theme_override_directory( 'fake' ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_template_file_path() passing a template file that doesn't exist in the theme. - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_template_file_path_template_file_not_in_theme() { - - /** - * Simulate the activation of a theme with the templates directory overridden. - */ - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake' ); - update_option( 'stylesheet', 'fake' ); - $this->_create_theme_override_directory( 'fake' ); - - $this->assertEquals( - llms()->plugin_path() . '/templates/single-certificate.php', - llms_template_file_path( 'single-certificate.php' ) - ); - - $this->_delete_theme_override_directory( 'fake' ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Test llms_template_file_path() when passing an absolute template directory (not relative to the plugin dir). - * - * @since 5.9.0 - * - * @return void - */ - public function test_llms_template_file_path_template_directory_absolute() { - $this->_delete_theme_override_directory( 'fake' ); - - $this->assertEquals( - '/path/to/absolute/single-certificate.php', - llms_template_file_path( 'single-certificate.php', 'path/to/absolute', true ) - ); - - /** - * Simulate the activation of a theme with the templates directory overridden. - */ - $original_template = get_option( 'template', '' ); - $original_stylesheet = get_option( 'stylesheet', '' ); - update_option( 'template', 'fake' ); - update_option( 'stylesheet', 'fake' ); - $this->_create_theme_override_directory( 'fake' ); - wp_cache_delete( 'theme-override-directories', 'llms_template_functions' ); - - $this->assertEquals( - get_theme_root() . '/fake/lifterlms/', - llms_template_file_path( '', 'path/to/absolute', true ) - ); - - $this->_delete_theme_override_directory( 'fake' ); - - update_option( 'template', $original_template ); - update_option( 'stylesheet', $original_stylesheet ); - - } - - /** - * Creates a theme and override lifterlms template directory. - * - * @since 4.8.0 - * @since 5.9.0 always remove the theme directory if it already exists. - * - * @param string $theme_dir_name Theme directory name. - * @return void - */ - private function _create_theme_override_directory( $theme_dir_name ) { - $theme_root = get_theme_root(); - $this->_delete_theme_override_directory( 'fake' ); - mkdir( "{$theme_root}/{$theme_dir_name}/lifterlms", 0777, true ); - } - - /** - * Deletes a theme and override lifterlms template directory. - * - * @since 4.8.0 - * - * @param string $theme_dir_name Theme directory name. - * @return void - */ - private function _delete_theme_override_directory( $theme_dir_name ) { - $theme_root = get_theme_root(); - if ( is_dir( "{$theme_root}/{$theme_dir_name}/lifterlms" ) ) { - rmdir( "{$theme_root}/{$theme_dir_name}/lifterlms" ); - } - if ( is_dir( "{$theme_root}/{$theme_dir_name}" ) ) { - rmdir( "{$theme_root}/{$theme_dir_name}" ); - } - } -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-loop.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-loop.php deleted file mode 100644 index 3f6372b86f..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-loop.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php -/** - * Test page functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_loop - * - * @since 3.38.0 - */ -class LLMS_Test_Functions_Loop extends LLMS_UnitTestCase { - - /** - * Test lifterlms_get_archive_description() and lifterlms_archive_description() on course and course taxonomy catalogs. - * - * @since 4.10.0 - * - * @return void - */ - public function test_lifterlms_get_archive_description_courses() { - - LLMS_Install::create_pages(); - - // On courses page with no description. - $this->go_to( get_post_type_archive_link( 'course' ) ); - $this->assertEquals( '', lifterlms_get_archive_description() ); - $this->assertEquals( '', $this->get_output( 'lifterlms_archive_description' ) ); - - // On courses page with a description. - wp_update_post( array( - 'ID' => llms_get_page_id( 'courses' ), - 'post_content' => 'Archive Description', - ) ); - $this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) ); - - // On a tax archive page with no tax description. - $term = wp_insert_term( 'mock-cat', 'course_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) ); - - // On a tax archive page with a tax description. - $term = wp_insert_term( 'mock-cat-with-desc', 'course_cat', array( 'description' => 'Term desc.' ) ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertEquals( llms_content( 'Term desc.' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Term desc.' ), $this->get_output( 'lifterlms_archive_description' ) ); - - } - - /** - * Test lifterlms_get_archive_description() and lifterlms_archive_description() on membership and membership taxonomy catalogs. - * - * @since 4.10.0 - * - * @return void - */ - public function test_lifterlms_get_archive_description_memberships() { - - LLMS_Install::create_pages(); - - // On courses page with no description. - $this->go_to( get_post_type_archive_link( 'llms_membership' ) ); - $this->assertEquals( '', lifterlms_get_archive_description() ); - $this->assertEquals( '', $this->get_output( 'lifterlms_archive_description' ) ); - - // On courses page with a description. - wp_update_post( array( - 'ID' => llms_get_page_id( 'memberships' ), - 'post_content' => 'Archive Description', - ) ); - $this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) ); - - // On a tax archive page with no tax description. - $term = wp_insert_term( 'mock-cat', 'membership_cat' ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) ); - - // On a tax archive page with a tax description. - $term = wp_insert_term( 'mock-cat-with-desc', 'membership_cat', array( 'description' => 'Term desc.' ) ); - $this->go_to( get_term_link( $term['term_id'] ) ); - $this->assertEquals( llms_content( 'Term desc.' ), lifterlms_get_archive_description() ); - $this->assertEquals( llms_content( 'Term desc.' ), $this->get_output( 'lifterlms_archive_description' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-pricing-table.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-pricing-table.php deleted file mode 100644 index 6bda04c8aa..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-pricing-table.php +++ /dev/null @@ -1,274 +0,0 @@ -<?php -/** - * Test product pricing table template functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group functions_template - * @group functions_template_product - * - * @since 3.38.0 - */ -class LLMS_Test_Functions_Templates_Pricing_Table extends LLMS_UnitTestCase { - - // Uncovered methods. - - // public function test_llms_get_access_plan_classes() {} - // public function test_llms_template_access_plan() {} - // public function test_llms_template_access_plan_button() {} - // public function test_llms_template_access_plan_description() {} - // public function test_llms_template_access_plan_feature() {} - // public function test_llms_template_access_plan_pricing() {} - // public function test_llms_template_access_plan_restrictions() {} - // public function test_llms_template_access_plan_title() {} - // public function test_llms_template_access_plan_trial() {} - - - /** - * Test lifterlms_template_pricing_table(): gateways disabled so we should show only free plans. - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_free_only() { - - $this->setManualGatewayStatus( 'no' ); - - $plan = $this->get_mock_plan( 0 ); - - $actions = did_action( 'llms_access_plan' ); - - $output = $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ); - - // Check HTML output. - $this->assertStringContains( sprintf( 'id="llms-access-plan-%d"', $plan->get( 'id' ) ), $output ); - $this->assertStringContains( sprintf( '<h4 class="llms-access-plan-title">%s</h4>', $plan->get( 'title' ) ), $output ); - $this->assertStringContains( 'FREE', $output ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'llms_access_plan' ) ); - - } - - /** - * Test lifterlms_template_pricing_table(): paid plan with gateways enabled. - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_purchasable() { - - $this->setManualGatewayStatus( 'yes' ); - - $plan = $this->get_mock_plan(); - - $actions = did_action( 'llms_access_plan' ); - - $output = $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ); - - // Check HTML output. - $this->assertStringContains( sprintf( 'id="llms-access-plan-%d"', $plan->get( 'id' ) ), $output ); - $this->assertStringContains( sprintf( '<h4 class="llms-access-plan-title">%s</h4>', $plan->get( 'title' ) ), $output ); - $this->assertStringContains( (string) $plan->get( 'price' ), $output ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'llms_access_plan' ) ); - - $this->setManualGatewayStatus( 'no' ); - - } - - /** - * Test lifterlms_template_pricing_table(): paid plan with no enabled gateways. - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_not_purchasable() { - - $this->setManualGatewayStatus( 'no' ); - - $plan = $this->get_mock_plan(); - - $actions = did_action( 'lifterlms_product_not_purchasable' ); - - // Empty output (just a bunch of new lines, actually). - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) ); - - $this->assertEquals( '', $output ); - - } - - /** - * Test lifterlms_template_pricing_table(): course enrollment start is in future. - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_err_enrollment_period_starts_in_future() { - - $plans = array( - $this->get_mock_plan(), - $this->get_mock_plan( 0 ), - ); - - foreach ( $plans as $plan ) { - - $course = llms_get_post( $plan->get( 'product_id' ) ); - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) ); - $course->set( 'enrollment_opens_message', 'Enrollment closed.' ); - - $actions = did_action( 'lifterlms_product_not_purchasable' ); - - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // Test HTML output. - $this->assertStringContains( 'class="llms-notice llms-error"', $output ); - $this->assertStringContains( 'Enrollment closed.', $output ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) ); - - } - - } - - /** - * Test lifterlms_template_pricing_table(): course enrollment start is in past. - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_err_enrollment_period_starts_in_past() { - - $plans = array( - $this->get_mock_plan(), - $this->get_mock_plan( 0 ), - ); - - foreach ( $plans as $plan ) { - - $course = llms_get_post( $plan->get( 'product_id' ) ); - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_end_date', date( 'Y-m-d h:i:s', strtotime( '-1 day' ) ) ); - $course->set( 'enrollment_closed_message', 'Enrollment closed.' ); - - $actions = did_action( 'lifterlms_product_not_purchasable' ); - - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // Test HTML output. - $this->assertStringContains( 'class="llms-notice llms-error"', $output ); - $this->assertStringContains( 'Enrollment closed.', $output ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) ); - - } - - } - - /** - * Test lifterlms_template_pricing_table(): course capacity maxed error - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_err_capacity() { - - $plans = array( - $this->get_mock_plan(), - $this->get_mock_plan( 0 ), - ); - - foreach ( $plans as $plan ) { - $course = llms_get_post( $plan->get( 'product_id' ) ); - $course->set( 'enable_capacity', 'yes' ); - $course->set( 'capacity', 1 ); - $course->set( 'capacity_message', 'No more room.' ); - - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - - $actions = did_action( 'lifterlms_product_not_purchasable' ); - - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // Test HTML output. - $this->assertStringContains( 'class="llms-notice llms-error"', $output ); - $this->assertStringContains( 'No more room.', $output ); - - // Action ran. - $this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) ); - } - - } - - /** - * Test lifterlms_template_pricing_table(): user already enrolled in course - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_enrolled_course() { - - $plan = $this->get_mock_plan(); - $course = llms_get_post( $plan->get( 'product_id' ) ); - $student = $this->get_mock_student(); - - $student->enroll( $course->get( 'id' ) ); - wp_set_current_user( $student->get( 'id' ) ); - - // Empty output (just a bunch of new lines, actually). - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // No actions ran. - $this->assertEquals( 0, did_action( 'lifterlms_product_not_purchasable' ) ); - $this->assertEquals( 0, did_action( 'llms_access_plan' ) ); - - $this->assertEquals( '', $output ); - - - } - - /** - * Test lifterlms_template_pricing_table(): user already enrolled in membership - * - * @since 3.38.0 - * - * @return void - */ - public function test_lifterlms_template_pricing_table_enrolled_membership() { - - $plan = $this->get_mock_plan(); - $membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ); - $student = $this->get_mock_student(); - - $plan->set( 'product_id', $membership_id ); - $student->enroll( $membership_id ); - wp_set_current_user( $student->get( 'id' ) ); - - // Empty output (just a bunch of new lines, actually). - $output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) ); - - // No actions ran. - $this->assertEquals( 0, did_action( 'lifterlms_product_not_purchasable' ) ); - $this->assertEquals( 0, did_action( 'llms_access_plan' ) ); - - $this->assertEquals( '', $output ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-information-fields.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-information-fields.php deleted file mode 100644 index 01b39be02c..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-information-fields.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Test user information field functions - * - * @package LifterLMS/Tests/Functions - * - * @group functions - * @group user_info_fields - * - * @since 5.0.0 - */ -class LLMS_Test_Functions_User_Info_fields extends LLMS_UnitTestCase { - - /** - * Test llms_get_user_information_field() - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_user_information_field() { - - // Does not exist. - $this->assertFalse( llms_get_user_information_field( 'fake' ) ); - - // Does exist. - $field = llms_get_user_information_field( 'email_address' ); - - $this->assertEquals( array( 'id', 'name', 'type', 'label', 'data_store', 'data_store_key' ), array_keys( $field ) ); - - $this->assertEquals( 'user_email', $field['data_store_key'] ); - $this->assertEquals( 'users', $field['data_store'] ); - $this->assertEquals( 'email_address', $field['name'] ); - $this->assertEquals( 'email_address', $field['id'] ); - - } - - /** - * Test llms_get_user_information_fields() - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_get_user_information_fields() { - - $list = llms_get_user_information_fields(); - - $ids = array(); - - foreach ( $list as $field ) { - - $this->assertArrayHasKey( 'id', $field ); - $this->assertArrayHasKey( 'data_store', $field ); - $this->assertArrayHasKey( 'data_store_key', $field ); - - $ids[] = $field['id']; - - } - - $expected_ids = array( - 'user_login', - 'email_address', - 'password', - 'first_name', - 'last_name', - 'display_name', - 'llms_billing_address_1', - 'llms_billing_address_2', - 'llms_billing_city', - 'llms_billing_country', - 'llms_billing_state', - 'llms_billing_zip', - 'llms_phone', - ); - $this->assertEquals( $expected_ids, $ids ); - } - -} diff --git a/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-postmeta.php b/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-postmeta.php deleted file mode 100644 index bf05201557..0000000000 --- a/tests/phpunit/unit-tests/functions/class-llms-test-functions-user-postmeta.php +++ /dev/null @@ -1,236 +0,0 @@ -<?php -/** - * Tests for LifterLMS User Postmeta functions - * - * @package LifterLMS/Tests - * - * @group functions - * @group user_postmeta - * - * @since 3.21.0 - * @since 3.33.0 Add test for the `llms_bulk_delete_user_postmeta` function. - * @since 4.5.1 Fix failing `test_delete_user_postmeta()` which was comparing based on array order when that doesn't strictly matter. - * @version 5.4.1 - */ -class LLMS_Test_Functions_User_Postmeta extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since Unknown - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - - $this->student = $this->get_mock_student(); - $this->student_id = $this->student->get( 'id' ); - $this->course_id = $this->generate_mock_courses( 1, 1, 3 )[0]; - $this->student->enroll( $this->course_id ); - - } - - public function test__llms_query_user_postmeta() { - - // fake user, fake post, fake key - $this->assertEquals( array(), _llms_query_user_postmeta( 123, 456, '_fake_val' ) ); - - // real user, fake post, fake key - $this->assertEquals( array(), _llms_query_user_postmeta( $this->student_id, 123, '_fake_val' ) ); - - // fake user, real post, fake key - $this->assertEquals( array(), _llms_query_user_postmeta( 123, $this->course_id, '_fake_val' ) ); - - // fake user, fake post, real key - $this->assertEquals( array(), _llms_query_user_postmeta( 123, 456, '_status' ) ); - - // has a result - $this->assertEquals( 1, count( _llms_query_user_postmeta( $this->student_id, $this->course_id, '_status' ) ) ); - - // find a specific value - $this->assertEquals( 1, count( _llms_query_user_postmeta( $this->student_id, $this->course_id, '_status', 'enrolled' ) ) ); - - // no key has more results - $this->assertEquals( 3, count( _llms_query_user_postmeta( $this->student_id, $this->course_id ) ) ); - - } - - /** - * Test llms_delete_user_postmeta() - * - * @since Unknown - * @since 4.5.1 Compare data as equal sets in favor of strict order comparison. - * - * @return void - */ - public function test_delete_user_postmeta() { - - // with a value - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' ); - $this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) ); - - // without a value - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' ); - $this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) ); - - // delete all non-unique vals - $i = 1; - while( $i <= 3 ) { - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' . $i, false ); - $i++; - } - $this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) ); - - // delete only a specific non unique value - $i = 1; - while( $i <= 3 ) { - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' . $i, false ); - $i++; - } - $this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme3' ) ); - $this->assertEqualSets( array( 'eraseme1', 'eraseme2' ), llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', false ) ); - - // delete all user post meta for student & post - $i = 1; - while( $i <= 3 ) { - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' . $i, 'eraseme', false ); - $i++; - } - $this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id ) ); - $this->assertEquals( array(), llms_get_user_postmeta( $this->student_id, $this->course_id ) ); - - } - - /** - * Test the bulk_delete_user_postmeta() method. - * - * @since 3.33.0 - * - * @return void - */ - public function test_bulk_delete_user_postmeta() { - - // delete the (key,value) matching pairs - $data = array( - '_bulk_test_data_to_erase1' => 'bulk_eraseme_1', - '_bulk_test_data_to_erase2' => 'bulk_eraseme_2', - '_bulk_test_data_to_preserve' => 'bulk_saveme_1', - '_bulk_test_data_to_preserve_value' => 'bulk_savemy_value_1', - ); - llms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data ); - - $data_to_erase = array( - '_bulk_test_data_to_erase1' => 'bulk_eraseme_1', - '_bulk_test_data_to_erase2' => 'bulk_eraseme_2', - '_bulk_test_data_to_preserve_value' => 'bulk_eraseme_3', - ); - - $this->assertEquals( array( - '_bulk_test_data_to_erase1' => true, - '_bulk_test_data_to_erase2' => true, - '_bulk_test_data_to_preserve_value' => false, - ), llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id, $data_to_erase ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_erase1' ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_erase2' ) ); - $this->assertEquals( $data['_bulk_test_data_to_preserve'], llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_preserve' ) ); - $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' ) ); - - // delete all the metas for a student and course - $data = array( - '_bulk_test_data_to_erase1' => 'bulk_eraseme_1', - '_bulk_test_data_to_erase2' => 'bulk_eraseme_2', - '_bulk_test_data_to_erase3' => 'bulk_eraseme_3', - '_bulk_test_data_to_erase4' => 'bulk_eraseme_4', - ); - llms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data ); - - $this->assertTrue( llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id ) ); - $this->assertEquals( array(), llms_get_user_postmeta( $this->student_id, $this->course_id ) ); - - // delete all the metas with the matching keys - $data = array( - '_bulk_test_data_to_erase1' => 'bulk_eraseme_1', - '_bulk_test_data_to_erase2' => 'bulk_eraseme_2', - '_bulk_test_data_to_erase3' => 'bulk_eraseme_3', - '_bulk_test_data_to_preserve' => 'bulk_saveme_1', - ); - llms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data ); - - $data_to_erase = array( - '_bulk_test_data_to_erase1' => null, - '_bulk_test_data_to_erase2' => null, - '_bulk_test_data_to_erase3' => null, - ); - $this->assertTrue( llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id, $data_to_erase ) ); - $this->assertArrayHasKey( '_bulk_test_data_to_preserve', llms_get_user_postmeta( $this->student_id, $this->course_id ) ); - - } - - - /** - * Test llms_get_user_postmeta(). - * - * @since 3.21.0 - * @since 5.3.2 Add delta when comparing enrollment date with updated date. - * @since 5.4.1 Compare dates using UNIX timestamps. - */ - public function test_llms_get_user_postmeta() { - - $this->assertEquals( 'enrolled', llms_get_user_postmeta( $this->student_id, $this->course_id, '_status' ) ); - $this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_fake' ) ); - $this->assertEquals( 3, count( llms_get_user_postmeta( $this->student_id, $this->course_id ) ) ); - - // Test serialized values. - $data = range( 1, 5 ); - llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_serialized_data', $data ); - $this->assertEquals( $data, llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_serialized_data' ) ); - - // Test updated date. - $enrollment_date = $this->student->get_enrollment_date( $this->course_id, 'enrolled', 'U' ); - $updated_date = llms_get_user_postmeta( $this->student_id, $this->course_id, '_status', true, 'updated_date' ); - $this->assertEqualsWithDelta( $enrollment_date, strtotime( $updated_date ), 2 ); - - } - - public function test_llms_update_user_postmeta() { - - // simple set and get - $this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval' ) ); - $this->assertEquals( 'testval', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key' ) ); - - // update that same val - $this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval2' ) ); - $this->assertEquals( 'testval2', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key' ) ); - - // should only have one for the key - $this->assertEquals( 1, count( llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key', false ) ) ); - - // add another but non unique - $this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval2-1', false ) ); - - // should be 2 now - $this->assertEquals( 2, count( llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key', false ) ) ); - - } - - public function test_llms_bulk_update_user_postmeta() { - - $data = array( - 'bulk_key1' => 'bulk_val1', - 'bulk_key2' => 'bulk_val2', - ); - - $this->assertTrue( llms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data ) ); - foreach ( $data as $key => $val ) { - $this->assertEquals( $val, llms_get_user_postmeta( $this->student_id, $this->course_id, $key ) ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-400.php b/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-400.php deleted file mode 100644 index 752e5bbf8f..0000000000 --- a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-400.php +++ /dev/null @@ -1,95 +0,0 @@ -<?php -/** - * Test update to 4.0.0 functions - * - * @package LifterLMS/Tests/Functions/Updates - * - * @group functions - * @group updates - * @group updates_400 - * - * @since 4.0.0 - */ -class LLMS_Test_Functions_Updates_400 extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * Include update functions file. - * - * @since 4.0.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-400.php'; - } - - /** - * Test llms_update_400_remove_session_options() - * - * @since 4.0.0 - * - * @return void - */ - public function test_remove_session_options() { - - $i = 0; - while ( $i < 20 ) { - $string = uniqid(); - add_option( '_wp_session_' . $string, array( 'data' ) ); - add_option( '_wp_session_expires' . $string, time() + HOUR_IN_SECONDS ); - ++$i; - } - - global $wpdb; - $sql = "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_wp_session_%';"; - - $this->assertEquals( 40, $wpdb->get_var( $sql ) ); - - llms_update_400_remove_session_options(); - - $this->assertEquals( 0, $wpdb->get_var( $sql ) ); - - } - - /** - * Test llms_update_400_clear_session_cron() - * - * @since 4.0.0 - * - * @return void - */ - public function test_clear_session_cron() { - - wp_schedule_event( time(), 'daily', 'wp_session_garbage_collection' ); - - llms_update_400_clear_session_cron(); - - $this->assertFalse( wp_next_scheduled( 'wp_session_garbage_collection' ) ); - - } - - /** - * Test llms_update_400_update_db_version() - * - * @since 4.0.0 - * - * @return void - */ - public function test_update_db_version() { - - $orig = get_option( 'lifterlms_db_version' ); - - llms_update_400_update_db_version(); - - $this->assertEquals( '4.0.0', get_option( 'lifterlms_db_version' ) ); - - update_option( 'lifterlms_db_version', $orig ); - - } - - -} diff --git a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-4150.php b/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-4150.php deleted file mode 100644 index ce55070730..0000000000 --- a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-4150.php +++ /dev/null @@ -1,233 +0,0 @@ -<?php -/** -* Test updates functions when updating to 4.15.0 - * - * @package LifterLMS/Tests/Functions/Updates - * - * @group functions - * @group updates - * @group updates_4150 - * - * @since 4.15.0 - * @version 4.15.0 - */ -class LLMS_Test_Functions_Updates_4150 extends LLMS_UnitTestCase { - - private $sessions; - - /** - * Setup before class - * - * Include update functions file. - * - * @since 4.15.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes and move teardown functions into here. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-4150.php'; - - // Clean posts and postmeta tables. - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->postmeta}" ); - $wpdb->query( "TRUNCATE TABLE {$wpdb->posts}" ); - // Delete transients. - delete_transient( 'llms_update_4150_remove_orphan_access_plans' ); - - } - - /** - * Test llms_update_4150_remove_orphan_access_plans - * - * @since 4.15.0 - * - * @return void - */ - public function test_update_4150_remove_orphan_access_plans() { - - // Create orphan access plans. - $access_plan_ids = $this->factory->post->create_many( - 10, - array( - 'post_type' => 'llms_access_plan', - ) - ); - foreach ( $access_plan_ids as $access_plan_id ) { - update_post_meta( $access_plan_id, '_llms_product_id', end( $access_plan_ids ) + 1 ); - } - - $this->assertEquals( - 10, - count( - get_posts( - array( - 'include' => $access_plan_ids, - 'post_type' => 'llms_access_plan', - ) - ) - ) - ); - - // Fire the update. - llms_update_4150_remove_orphan_access_plans(); - - // Expect no orphan access plans. - $this->assertEquals( - 0, - count( - get_posts( - array( - 'include' => $access_plan_ids, - 'post_type' => 'llms_access_plan', - ) - ) - ) - ); - - } - - /** - * Test llms_update_4150_remove_orphan_access_plans - * - * @since 4.15.0 - * - * @return void - */ - public function test_update_4150_remove_orphan_access_plans_keep_linked() { - - // Create linked access plans. - $access_plan_ids = $this->factory->post->create_many( - 11, - array( - 'post_type' => 'llms_access_plan', - ) - ); - - $course = $this->factory->post->create(); - foreach ( $access_plan_ids as $access_plan_id ) { - update_post_meta( $access_plan_id, '_llms_product_id', $course ); - } - - // Create orphan access plans. - $orphan_access_plan_ids = $this->factory->post->create_many( - 10, - array( - 'post_type' => 'llms_access_plan', - ) - ); - - foreach ( $orphan_access_plan_ids as $access_plan_id ) { - update_post_meta( $access_plan_id, '_llms_product_id', end( $orphan_access_plan_ids ) + 1 ); - } - - llms_update_4150_remove_orphan_access_plans(); - - // Expect no orphan access plans. - $this->assertEquals( - 0, - count( - get_posts( - array( - 'include' => $orphan_access_plan_ids, - 'post_type' => 'llms_access_plan', - ) - ) - ) - ); - - // Expect linked access plans are still there. - $this->assertEquals( - count( $access_plan_ids ), - count( - get_posts( - array( - 'include' => $access_plan_ids, - 'post_type' => 'llms_access_plan', - ) - ) - ) - ); - - } - - /** - * Test "pagination" in llms_update_4150_remove_orphan_access_plans() - * - * @since 4.15.0 - * - * @return void - */ - public function test_update_4150_remove_orphan_access_plans_pagination() { - - // Create orphan access plans. - $orphan_access_plan_ids = $this->factory->post->create_many( - 110, // Each page is of 50 orphan access plans. - array( - 'post_type' => 'llms_access_plan', - ) - ); - - foreach ( $orphan_access_plan_ids as $access_plan_id ) { - update_post_meta( $access_plan_id, '_llms_product_id', end( $orphan_access_plan_ids ) + 1 ); - } - - $loops = 0; - // Check how many times the update function needs to run. - // Internally we fetch 50 orphan access plans at time, we expect it to run the following number of times: - $expected_loops = 3; - while ( llms_update_4150_remove_orphan_access_plans() ) { - $loops++; - } - - $this->assertEquals( $expected_loops, $loops ); - $this->assertEquals( get_transient( 'llms_update_4150_remove_orphan_access_plans' ), 'complete' ); - - // Expect no orphan access plans. - $this->assertEquals( - 0, - count( - get_posts( - array( - 'include' => $orphan_access_plan_ids, - 'post_type' => 'llms_access_plan', - 'numberposts' => 200 - ) - ) - ) - ); - - } - - - /** - * Test llms_update_4150_update_db_version() - * - * @since 4.15.0 - * - * @return void - */ - public function test_update_db_version() { - - $orig = get_option( 'lifterlms_db_version' ); - - // Remove existing db version. - delete_option( 'lifterlms_db_version' ); - - llms_update_4150_update_db_version(); - - $this->assertNotEquals( '4.15.0', get_option( 'lifterlms_db_version' ) ); - - // Unlock the db version update. - set_transient( 'llms_update_4150_remove_orphan_access_plans', 'complete', DAY_IN_SECONDS ); - - llms_update_4150_update_db_version(); - - $this->assertEquals( '4.15.0', get_option( 'lifterlms_db_version' ) ); - - update_option( 'lifterlms_db_version', $orig ); - - } - -} diff --git a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-450.php b/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-450.php deleted file mode 100644 index 2e044b9fdf..0000000000 --- a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-450.php +++ /dev/null @@ -1,216 +0,0 @@ -<?php -/** -* Test updates functions when updating to 4.5.0 - * - * @package LifterLMS/Tests/Functions/Updates - * - * @group functions - * @group updates - * @group updates_450 - * - * @since 4.5.0 - * @version 4.15.0 - */ -class LLMS_Test_Functions_Updates_450 extends LLMS_UnitTestCase { - - private $sessions; - - /** - * Setup before class - * - * Include update functions file. - * - * @since 4.5.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-450.php'; - } - - /** - * Setup the test case - * - * @since 4.5.0 - * @since 5.3.3 Renamed setUp() to set_up() and moved teardown functions into here. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->sessions = LLMS_Sessions::instance(); - - // Clean open sessions table. - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions" ); - // Delete transients. - delete_transient( 'llms_update_450_migrate_events_open_sessions' ); - delete_transient( 'llms_450_skipper_events_open_sessions' ); - } - - /** - * Test llms_update_450_migrate_events_open_sessions() - * - * @since 4.5.0 - * - * @return void - */ - public function test_migrate_events_open_sessions() { - // Create open session events. - $open_session_ids = $this->create_open_session_events( 10, 3 ); - - // Fire the update. - llms_update_450_migrate_events_open_sessions(); - - // Get the migrated open sessions. - $open_sessions = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_open_sessions' ); - - // Expect 7 open sessions. - $this->assertEquals( 7, count( $open_sessions ) ); - $this->assertEquals( count( $open_session_ids ), count( $open_sessions ) ); - - // Expect their ids match the not closed ones. - foreach ( $open_sessions as $os ) { - $this->assertContains( $os->get('id'), $open_session_ids ); - } - - } - - /** - * Test "pagination" in llms_update_450_migrate_events_open_sessions() - * - * @since 4.5.0 - * - * @return void - */ - public function test_migrate_events_open_sessions_pagination() { - - // Create open session events. - $num_open_sessions = 250; - $open_session_ids = $this->create_open_session_events( $num_open_sessions ); - - $loops = 1; - // Check how many times the update function needs to run. - // Internally we fetch 200 sessions at time, we expect it to run the following number of times: - $expected_loops = 3; - while ( llms_update_450_migrate_events_open_sessions() ) { - $loops++; - } - $this->assertEquals( $expected_loops, $loops ); - $this->assertEquals( get_transient( 'llms_450_skipper_events_open_sessions' ), $expected_loops * 200 ); - $this->assertEquals( get_transient( 'llms_update_450_migrate_events_open_sessions' ), 'complete' ); - - // Get the migrated open sessions. - global $wpdb; - $open_sessions = $wpdb->get_col( // db call ok; no-cache ok. - $wpdb->prepare( - " - SELECT event_id - FROM {$wpdb->prefix}lifterlms_events_open_sessions - ORDER BY event_id ASC - LIMIT %d, %d - ", - 0, - 300 - ) - ); - - // Expect all of them have been correctly migrated. - $this->assertEquals( $num_open_sessions, count( $open_sessions ) ); - - } - - /** - * Test llms_update_450_update_db_version() - * - * @since 4.5.0 - * @since 4.15.0 Get original db_version before removing it. - * - * @return void - */ - public function test_update_db_version() { - - $orig = get_option( 'lifterlms_db_version' ); - - // Remove existing db version. - delete_option( 'lifterlms_db_version' ); - - llms_update_450_update_db_version(); - - $this->assertNotEquals( '4.5.0', get_option( 'lifterlms_db_version' ) ); - - // Unlock the db version update. - set_transient( 'llms_update_450_migrate_events_open_sessions', 'complete', DAY_IN_SECONDS ); - - llms_update_450_update_db_version(); - - $this->assertEquals( '4.5.0', get_option( 'lifterlms_db_version' ) ); - - update_option( 'lifterlms_db_version', $orig ); - - } - - /** - * Util to create open sessions in the lifterlms_events table - * - * @since 4.5.0 - * - * @param int $num_open_sessions Number of sessions to open. - * @param int $num_closed_sessions Optional. Number of sessions to close. Default 0. - * @return int[] An array with the events ids of the still open sessions. - */ - private function create_open_session_events( $num_open_sessions, $num_closed_sessions = 0 ) { - $time = time(); - - $i = 1; - $open_session_ids = array(); - - while ( $i <= $num_open_sessions ) { - $user = $this->factory->user->create(); - wp_set_current_user( $user ); - - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - - $object_id = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id', array( $user ) ); - // Record session start. - $session_start = LLMS()->events()->record( - array( - 'actor_id' => $user, - 'object_type' => 'session', - 'object_id' => $object_id, - 'event_type' => 'session', - 'event_action' => 'start', - ) - ); - $open_session_ids[] = $session_start->get( 'id' ); - - // Close N sessions. - if ( $num_closed_sessions - && $num_closed_sessions <= $num_open_sessions - && $i > ( $num_open_sessions - $num_closed_sessions ) ) { - - $time += MINUTE_IN_SECONDS; - llms_tests_mock_current_time( $time ); - // Record session end. - LLMS()->events()->record( - array( - 'actor_id' => $user, - 'object_type' => 'session', - 'object_id' => $object_id, - 'event_type' => 'session', - 'event_action' => 'end', - ) - ); - array_pop( $open_session_ids ); - } - - $i++; - } - - return $open_session_ids; - } - -} diff --git a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-500.php b/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-500.php deleted file mode 100644 index bc446d1126..0000000000 --- a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-500.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php -/** -* Test updates functions when updating to 5.0.0 - * - * @package LifterLMS/Tests/Functions/Updates - * - * @group functions - * @group updates - * @group updates_500 - * - * @since 5.0.0 - * @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. - */ -class LLMS_Test_Functions_Updates_500 extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * Include update functions file. - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-500.php'; - } - - /** - * Test llms_update_500_legacy_options_autoload_off() method - * - * @since 5.0.0 - * - * @return void - */ - public function test_llms_update_500_legacy_options_autoload_off() { - - global $wpdb; - - $legacy_options_to_stop_autoloading = array( - 'lifterlms_registration_generate_username', - 'lifterlms_registration_password_strength', - 'lifterlms_registration_password_min_strength', - ); - - // Firs create them, by default they are autoloaded. - array_map( 'add_option', $legacy_options_to_stop_autoloading, array_fill( 0, count( $legacy_options_to_stop_autoloading ), 'yes' ) ); - - $check_options_query = "SELECT option_name FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $legacy_options_to_stop_autoloading ), '%s' ) ) . ')'; - $check_autoload_query = $check_options_query. ' AND autoload="yes"'; - - // Check they are autoloaded. - $this->assertEquals( count( $legacy_options_to_stop_autoloading ), $wpdb->query( $wpdb->prepare( $check_autoload_query, $legacy_options_to_stop_autoloading ) ) ); - - llms_update_500_legacy_options_autoload_off(); - - // Check they are not autoloaded anymore and check they exist :D. - $this->assertEquals( 0, $wpdb->query( $wpdb->prepare( $check_autoload_query, $legacy_options_to_stop_autoloading ) ) ); - $this->assertEqualSets( $legacy_options_to_stop_autoloading, $wpdb->get_col( $wpdb->prepare( $check_options_query, $legacy_options_to_stop_autoloading ) ) ); - - array_map( 'delete_option', $legacy_options_to_stop_autoloading ); - - } - - /** - * Test llms_update_500_update_db_version() - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_db_version() { - - $orig = get_option( 'lifterlms_db_version' ); - - // Remove existing db version. - delete_option( 'lifterlms_db_version' ); - - llms_update_500_update_db_version(); - - $this->assertEquals( '5.0.0', get_option( 'lifterlms_db_version' ) ); - - update_option( 'lifterlms_db_version', $orig ); - - } - - /** - * Test llms_update_500_add_admin_notice() - * - * @since 5.0.0 - * - * @return void - */ - public function test_update_500_add_admin_notice() { - - $notice = 'v500-welcome-msg'; - - require_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php'; - - $this->assertFalse( LLMS_Admin_Notices::has_notice( $notice ) ); - - llms_update_500_add_admin_notice(); - - $this->assertTrue( true, LLMS_Admin_Notices::has_notice( $notice ) ); - - // Cleanup. - LLMS_Admin_Notices::delete_notice( $notice ); - - } -} diff --git a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-520.php b/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-520.php deleted file mode 100644 index cff8150efc..0000000000 --- a/tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-520.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** -* Test updates functions when updating to 5.2.0 - * - * @package LifterLMS/Tests/Functions/Updates - * - * @group functions - * @group updates - * @group updates_520 - * - * @since 5.2.0 - */ -class LLMS_Test_Functions_Updates_520 extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * Include update functions file. - * - * @since 5.2.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - parent::set_up_before_class(); - require_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-520.php'; - } - - /** - * Test llms_update_520_upcoming_reminder_notification_backward_compat() method - * - * @since 5.2.0 - * - * @return void - */ - public function test_llms_update_520_upcoming_reminder_notification_backward_compat() { - - $subscribers_for_type = array( - 'email' => array( - 'student', - ), - 'basic' => array( - 'student', - 'author', - 'custom', - ), - ); - - foreach ( $subscribers_for_type as $type => $subscribers ) { - $this->assertEquals( - array(), - get_option( "llms_notification_upcoming_payment_reminder_{$type}_subscribers", array() ) - ); - } - - // Run the update. - llms_update_520_upcoming_reminder_notification_backward_compat(); - - foreach ( $subscribers_for_type as $type => $subscribers ) { - $this->assertEquals( - array_fill_keys( $subscribers, 'no' ), - get_option( "llms_notification_upcoming_payment_reminder_{$type}_subscribers", array() ) - ); - } - - // Create the option and check it's not overridden. - foreach ( $subscribers_for_type as $type => $subscribers ) { - update_option( "llms_notification_upcoming_payment_reminder_{$type}_subscribers", array_fill_keys( $subscribers, 'yes' ) ); - - $this->assertNotEquals( - array_fill_keys( $subscribers, 'no' ), - get_option( "llms_notification_upcoming_payment_reminder_{$type}_subscribers", array() ) - ); - } - - } - - /** - * Test llms_update_520_update_db_version() - * - * @since 5.2.0 - * - * @return void - */ - public function test_update_520_update_db_version() { - - $orig = get_option( 'lifterlms_db_version' ); - - // Remove existing db version. - delete_option( 'lifterlms_db_version' ); - - llms_update_520_update_db_version(); - - $this->assertEquals( '5.2.0', get_option( 'lifterlms_db_version' ) ); - - update_option( 'lifterlms_db_version', $orig ); - - } - -} diff --git a/tests/phpunit/unit-tests/integrations/class-llms-test-integration-bbpress.php b/tests/phpunit/unit-tests/integrations/class-llms-test-integration-bbpress.php deleted file mode 100644 index d2d1182d10..0000000000 --- a/tests/phpunit/unit-tests/integrations/class-llms-test-integration-bbpress.php +++ /dev/null @@ -1,649 +0,0 @@ -<?php -/** - * Tests for LLMS_Admin_Review class - * - * @package LifterLMS/Tests/Integrations - * - * @group integrations - * @group integration_bbpress - * - * @since 3.37.11 - * @since 3.38.1 Added test on forum values saved as array of strings. - */ -class LLMS_Test_Integration_BBPress extends LLMS_Unit_Test_Case { - - /** - * Instance of a mock bbPress class. - * - * @var bbPress - */ - protected $mock_bbPress = null; - - /** - * Instance of the bbPress integration class. - * - * @var LLMS_Integration_BBPress - */ - protected $main = null; - - /** - * Array of hooks added by the integration. - * - * @var array - */ - protected $hooks = array(); - - /** - * Add or remove hooks based on hooks defined in the $this->hooks array. - * - * @since 3.37.11 - * - * @param string $action Either "add" or "remove". - * @return void - */ - private function update_hooks( $action = 'add' ) { - - foreach ( $this->hooks as $hook ) { - - $function = sprintf( '%1$s_%2$s', $action, $hook['type'] ); - $function( $hook['hook'], $hook['method'], $hook['priority'] ); - - } - - } - - /** - * Run assertions for all hooks in the $this->hooks array. - * - * @since 3.37.11 - * - * @param mixed $equals If `null`, asserts that the priority matches the configured priority. Otherwise all hooks equal this value. - * @return void - */ - private function assertHooks( $equals = null ) { - - foreach( $this->hooks as $hook ) { - - $function = sprintf( 'has_%s', $hook['type'] ); - $this->assertEquals( is_null( $equals ) ? $hook['priority'] : $equals, $function( $hook['hook'], $hook['method'] ), $hook['hook'] ); - - } - - } - - /** - * Setup all the hooks defined in the configuration method. - * - * @since 3.37.11 - * - * @return void - */ - private function setup_hooks() { - - $this->hooks = array( - array( - 'type' => 'filter', - 'hook' => 'lifterlms_engagement_triggers', - 'method' => array( $this->main, 'register_engagement_triggers' ), - 'priority' => 10, - ), - array( - 'type' => 'filter', - 'hook' => 'lifterlms_external_engagement_query_arguments', - 'method' => array( $this->main, 'engagement_query_args' ), - 'priority' => 10, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_load_shortcodes', - 'method' => array( $this->main, 'register_shortcodes' ), - 'priority' => 10, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_membership_restricted_post_types', - 'method' => array( $this->main, 'add_membership_restrictions' ), - 'priority' => 10, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_page_restricted_before_check_access', - 'method' => array( $this->main, 'restriction_checks_memberships' ), - 'priority' => 40, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_page_restricted_before_check_access', - 'method' => array( $this->main, 'restriction_checks_courses' ), - 'priority' => 50, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_metabox_fields_lifterlms_course_options', - 'method' => array( $this->main, 'course_settings_fields' ), - 'priority' => 10, - ), - array( - 'type' => 'filter', - 'hook' => 'llms_get_course_properties', - 'method' => array( $this->main, 'add_course_props' ), - 'priority' => 10, - ), - - array( - 'type' => 'action', - 'hook' => 'bbp_new_topic', - 'method' => array( LLMS()->engagements(), 'maybe_trigger_engagement' ), - 'priority' => 10, - ), - array( - 'type' => 'action', - 'hook' => 'bbp_new_reply', - 'method' => array( LLMS()->engagements(), 'maybe_trigger_engagement' ), - 'priority' => 10, - ), - array( - 'type' => 'action', - 'hook' => 'llms_metabox_after_save_lifterlms-course-options', - 'method' => array( $this->main, 'save_course_settings' ), - 'priority' => 10, - ), - array( - 'type' => 'action', - 'hook' => 'llms_content_restricted_by_bbp_course_forum', - 'method' => array( $this->main, 'handle_course_forum_restriction' ), - 'priority' => 10, - ), - ); - - } - - /** - * Setup the mock bbPress class and functions. - * - * @since 3.37.11 - * - * @return void - */ - protected function setup_mock_bbPress() { - - // Mock functions. - if ( ! function_exists( 'bbp_get_forum_post_type' ) ) { - function bbp_get_forum_post_type() { - return 'mock_forum_post_type'; - } - } - - // Create the mock bbPress class. - $this->mock_bbPress = $this->getMockBuilder( 'bbPress' ) - ->getMock(); - - // Enable the integration. - update_option( 'llms_integration_bbpress_enabled', 'yes' ); - - // Refresh cached available integrations list. - LLMS()->integrations()->get_available_integrations(); - - } - - /** - * Setup the test case. - * - * @since 3.37.11 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - - // Load mock. - if ( ! $this->mock_bbPress ) { - $this->setup_mock_bbPress(); - } - - $this->main = LLMS()->integrations()->get_integration( 'bbpress' ); - - if ( ! $this->hooks ) { - $this->setup_hooks(); - } - - } - - /** - * Test that attributes are setup properly. - * - * @since 3.37.11 - * - * @return void - */ - public function test_attributes() { - - $this->assertEquals( 'bbPress', $this->main->title ); - $this->assertTrue( ! empty( $this->main->description ) ); - - } - - /** - * Test configure() - * - * @since 3.37.11 - * - * @return void - */ - public function test_configure() { - - // Disable the integration. - update_option( 'llms_integration_bbpress_enabled', 'no' ); - - // Remove all the set hooks. - $this->update_hooks( 'remove' ); - - LLMS_Unit_Test_Util::call_method( $this->main, 'configure' ); - - // All hooks should be false when calling has_action()/has_filter(). - $this->assertHooks( false ); - - - // Re-enable the integration. - $this->setup_mock_bbPress(); - LLMS_Unit_Test_Util::call_method( $this->main, 'configure' ); - - // All hooks should be configured. - $this->assertHooks(); - - } - - /** - * Test add_course_props() - * - * @since 3.37.11 - * - * @return void - */ - public function test_add_course_props() { - - $this->assertEquals( array( 'bbp_forum_ids' => 'array' ), $this->main->add_course_props( array(), 'mock' ) ); - - } - - /** - * Test add_membership_restrictions() - * - * @since 3.37.11 - * - * @return void - */ - public function test_add_membership_restrictions() { - - $this->assertEquals( array( 'mock_forum_post_type' ), $this->main->add_membership_restrictions( array() ) ); - - } - - /** - * Test course_settings_field() - * - * @since 3.37.11 - * - * @return void - */ - public function test_course_settings_fields() { - - $res = $this->main->course_settings_fields( array() )[0]; - - $this->assertEquals( 'bbPress', $res['title'] ); - $this->assertEquals( 1, count( $res['fields'] ) ); - - $this->assertequals( '_llms_bbp_forum_ids', $res['fields'][0]['id'] ); - - } - - /** - * Test engagement_query_args() for non bbPress hooks. - * - * @since 3.37.11 - * - * @return void - */ - public function test_engagement_query_args_not_supported() { - - $expect = array( - 'trigger_type' => 'mock', - 'related_post_id' => 123, - 'user_id' => 456, - ); - $this->assertEquals( $expect, $this->main->engagement_query_args( $expect, 'mock_action', array() ) ); - - } - - /** - * Test engagement_query_args() for a new reply hook. - * - * @since 3.37.11 - * - * @return void - */ - public function test_engagement_query_args_new_reply() { - - $args = array( - 'trigger_type' => 'mock', - 'related_post_id' => 123, - 'user_id' => 456, - ); - - $expect = array( - 'trigger_type' => 'bbp_new_reply', - 'related_post_id' => '', - 'user_id' => 4, - ); - - $this->assertEquals( $expect, $this->main->engagement_query_args( $args, 'bbp_new_reply', array( 0, 1, 2, 3, 4, ) ) ); - - } - - /** - * Test engagement_query_args() for a new topic - * - * @since 3.37.11 - * - * @return void - */ - public function test_engagement_query_args_new_topic() { - - $args = array( - 'trigger_type' => 'mock', - 'related_post_id' => 123, - 'user_id' => 456, - ); - - $expect = array( - 'trigger_type' => 'bbp_new_topic', - 'related_post_id' => '', - 'user_id' => 3, - ); - - $this->assertEquals( $expect, $this->main->engagement_query_args( $args, 'bbp_new_topic', array( 0, 1, 2, 3 ) ) ); - - } - - /** - * Test handle_course_forum_restriction() - * - * @since 3.37.11 - * - * @return void - */ - public function test_handle_course_forum_restriction() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $this->expectException( LLMS_Unit_Test_Exception_Redirect::class ); - $this->expectExceptionMessage( sprintf( '%s [302] YES', get_permalink( $id ) ) ); - - try { - - $this->main->handle_course_forum_restriction( array( 'restriction_id' => $id ) ); - - } catch( LLMS_Unit_Test_Exception_Redirect $exception ) { - - $notices = llms_get_notices(); - - $this->assertStringContains( 'llms-error', $notices ); - $this->assertStringContains( 'You must be enrolled in this course to access the course forum.', $notices ); - - throw $exception; - } - - } - - /** - * Test get_course_forum_ids() - * - * @since 3.37.11 - * - * @return void - */ - public function test_get_course_forum_ids() { - - // Ensure property filter is applied. - LLMS_Unit_Test_Util::call_method( $this->main, 'configure' ); - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - // Nothing set. - $this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) ); - - // Empty string. - update_post_meta( $id, '_llms_bbp_forum_ids', '' ); - $this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) ); - - // Empty array. - update_post_meta( $id, '_llms_bbp_forum_ids', array() ); - $this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) ); - - // Has forums. - update_post_meta( $id, '_llms_bbp_forum_ids', array( 1, 2, 3 ) ); - $this->assertEquals( array( 1, 2, 3 ), $this->main->get_course_forum_ids( $id ) ); - - } - - /** - * Test get_forum_course_restrictions() - * - * @since 3.37.11 - * @since 3.38.1 Made sure it's able to match forum ids either saved as strings or integers. - * - * @return void - */ - public function test_get_forum_course_restrictions() { - - // No restrictions for a fake forum. - $this->assertEquals( array(), $this->main->get_forum_course_restrictions( 3452 ) ); - - // Restricted to one course. - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - update_post_meta( $id, '_llms_bbp_forum_ids', array( 9239 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 9239 ) ); - - // Restricted to two courses. - $id2 = $this->factory->post->create( array( 'post_type' => 'course' ) ); - update_post_meta( $id2, '_llms_bbp_forum_ids', array( 9239 ) ); - $this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 9239 ) ); - - // Restricted to two courses, second course forum ids saved as strings. - update_post_meta( $id, '_llms_bbp_forum_ids', array( 9239, 1008 ) ); - update_post_meta( $id2, '_llms_bbp_forum_ids', array( '9239', '1008', '1007' ) ); - - // Make sure we don't match a forum id which is part of one of the saved values. - $this->assertNotEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 923 ) ); - - $this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 9239 ) ); - $this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 1008 ) ); - $this->assertEquals( array( $id2 ), $this->main->get_forum_course_restrictions( 1007 ) ); - - update_post_meta( $id2, '_llms_bbp_forum_ids', array( '1' ) ); - $this->assertEquals( array( $id2 ), $this->main->get_forum_course_restrictions( 1 ) ); - - /** - * Edge case check: - * We save the values as a serialized array, and before 3.37.11 we used to save them as integers. - * Our SQL query to retrieve the courses linked to a certain forum uses a REGEXP to match the forum id in it. - * This REGEXP is able to match either ids saved as strings or integers. - * We want also to be sure that if we have a value of the type - * a:3:{i:0;i:2299;i:1;i:3333;i:2:i:7777;} - * and a forum id to check against equal to 1, that value above doesn't match. - * This would mean that our query is able differentiate between serialized array item values and indexes. - */ - // Case saved as integers. - update_post_meta( $id, '_llms_bbp_forum_ids', array( 2299, 3333, 7777, 9999, 29999, 109999 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 2299 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 3333 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 7777 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 9999 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 29999 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 109999 ) ); - - - // Make sure we don't match the array indexes. - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 0 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 1 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 2 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 3 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 4 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 5 ) ); - - // Case saved as strings. - update_post_meta( $id, '_llms_bbp_forum_ids', array( '12299', '13333', '17777', '19999', '129999', '1109999' ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 12299 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 13333 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 17777 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 19999 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 129999 ) ); - $this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 1109999 ) ); - - // Make sure we don't match the array indexes - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 0 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 1 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 2 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 3 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 4 ) ); - $this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 5 ) ); - } - - /** - * Test is_installed() - * - * @since 3.37.11 - * - * @return void - */ - public function test_is_installed() { - - $this->assertTrue( $this->main->is_installed() ); - - } - - /** - * Test register_shortcodes() - * - * @since 3.37.11 - * - * @return void - */ - public function test_register_shortcodes() { - - $this->assertEquals( array( 'LLMS_BBP_Shortcode_Course_Forums_List' ), $this->main->register_shortcodes( array() ) ); - - } - - // @todo these tests should be written but I'm tired. - // public function test_restriction_checks_courses() {} - // public function test_restriction_checks_memberships() {} - - /** - * Test register_engagement_triggers() - * - * @since 3.37.11 - * - * @return void - */ - public function test_register_engagement_triggers() { - - $this->assertEquals( array( 'bbp_new_topic', 'bbp_new_reply' ), array_keys( $this->main->register_engagement_triggers( array() ) ) ); - - } - - /** - * Test save_course_settings() when a quick edit is performed on a course. - * - * @since 3.37.11 - * - * @return void - */ - public function test_save_course_settings_quick_edit() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $expect = array( 1, 2, 3 ); - update_post_meta( $id, '_llms_bbp_forum_ids', $expect ); - - $this->mockPostRequest( array( 'action' => 'inline-save' ) ); - $this->assertNull( $this->main->save_course_settings( $id ) ); - - $this->assertEquals( $expect, get_post_meta( $id, '_llms_bbp_forum_ids', true ) ); - - } - - /** - * Test save_course_settings() correctly saving strings - * - * @since 3.38.1 - * - * @return void - */ - public function test_save_course_settings_save_strings() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $expect = array( 1, 2, 3 ); - - $this->mockPostRequest( array( 'action' => '', '_llms_bbp_forum_ids' => $expect ) ); - $this->main->save_course_settings( $id ); - - $this->assertEquals( $expect, array_filter( get_post_meta( $id, '_llms_bbp_forum_ids', true ), 'is_string' ) ); - - } - - /** - * Test save_course_settings() when there's no new ids passed to the form. - * - * @since 3.37.11 - * - * @return void - */ - public function test_save_course_settings_not_set() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - update_post_meta( $id, '_llms_bbp_forum_ids', array( 1 ) ); - - $this->assertEquals( array(), $this->main->save_course_settings( $id ) ); - $this->assertEquals( array(), get_post_meta( $id, '_llms_bbp_forum_ids', true ) ); - - } - - /** - * Test save_course_settiongs() - * - * @since 3.37.11 - * - * @return void - */ - public function test_save_course_settings() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $expect = array( 1, 2, 3 ); - $this->mockPostRequest( array( '_llms_bbp_forum_ids' => $expect ) ); - - $this->assertEquals( $expect, $this->main->save_course_settings( $id ) ); - $this->assertEquals( $expect, get_post_meta( $id, '_llms_bbp_forum_ids', true ) ); - - } - - /** - * Test save_course_settings() to delete existing courses. - * - * @since 3.37.11 - * - * @return void - */ - public function test_save_course_settings_delete() { - - $id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - update_post_meta( $id, '_llms_bbp_forum_ids', array( 1 ) ); - $this->mockPostRequest( array( '_llms_bbp_forum_ids' => array() ) ); - - $this->assertEquals( array(), $this->main->save_course_settings( $id ) ); - $this->assertEquals( array(), get_post_meta( $id, '_llms_bbp_forum_ids', true ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-event.php b/tests/phpunit/unit-tests/models/class-llms-test-event.php deleted file mode 100644 index 85c8660fe4..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-event.php +++ /dev/null @@ -1,178 +0,0 @@ -<?php -/** - * Test INsturctor model - * - * @package LifterLMS_Tests/Models - * - * @group LLMS_Event - * @group events - * - * @since 3.36.0 - * @since 4.3.0 Add assertions to test against hooks and deprecated hooks. - * @since 5.3.3 Removed empty `setUp()` method. - */ -class LLMS_Test_Event extends LLMS_Unit_Test_Case { - - /** - * Teardown the test case. - * - * @since 3.36.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - parent::tear_down(); - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events" ); - } - - /** - * Test CRUD. - * - * @since 3.36.0 - * @since 4.3.0 Add update & deletion & added assertions against expected hooks. - * - * @return void - */ - public function test_crud() { - - $actions = did_action( 'llms_event_created' ); - - $expected_time = current_time( 'timestamp' ) - DAY_IN_SECONDS; - llms_tests_mock_current_time( $expected_time ); - - $args = array( - 'actor_id' => 1, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'load', - ); - - // Create. - $event = new LLMS_Event(); - $event->setUp( $args ); - $this->assertTrue( $event->save() ); - $id = $event->get( 'id' ); - $this->assertTrue( is_numeric( $id ) ); - - llms_tests_reset_current_time(); - - $event = new LLMS_Event( $id ); - - $this->assertEquals( $expected_time, strtotime( $event->get( 'date' ) ) ); - foreach( $args as $key => $expected ) { - $this->assertEquals( $expected, $event->get( $key ) ); - } - - $this->assertEquals( ++$actions, did_action( 'llms_event_created' ) ); - $this->assertEquals( 0, did_action( 'llms___created' ) ); - - // Update. - $actions = did_action( 'llms_event_updated' ); - $event->set( 'actor_id', 2, true ); - - $this->assertEquals( ++$actions, did_action( 'llms_event_updated' ) ); - $this->assertEquals( 0, did_action( 'llms__updated' ) ); - - $event = new LLMS_Event( $id ); - $this->assertEquals( 2, $event->get( 'actor_id' ) ); - - // Delete. - $actions = did_action( 'llms_event_deleted' ); - $this->assertTrue( $event->delete() ); - $this->assertEquals( ++$actions, did_action( 'llms_event_deleted' ) ); - $this->assertEquals( 0, did_action( 'llms__deleted' ) ); - - } - - /** - * Test metadata getters, setters, unsetters. - * - * @since 3.36.0 - * - * @return void - */ - public function test_meta() { - - $args = array( - 'actor_id' => 1, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'load', - ); - - $meta = array( - 'meta_key' => 'meta_val', - 'another' => 1, - ); - - $event = new LLMS_Event(); - - // Set multiple metas. - $event->setUp( $args )->set_metas( $meta ); - - // Get all metas. - $this->assertEquals( $meta, $event->get_meta() ); - - // Get individual metas. - foreach ( $meta as $key => $expect ) { - - $this->assertEquals( $expect, $event->get_meta( $key ) ); - - } - - // Update a single meta value. - $event->set_meta( 'meta_key', 'new_val' ); - $this->assertEquals( 'new_val', $event->get_meta( 'meta_key' ) ); - - // Create a new meta item. - $event->set_meta( 'new_key', true ); - $this->assertTrue( $event->get_meta( 'new_key' ) ); - - // Delete a single meta item. - $event->delete_meta( 'new_key' ); - $this->assertNull( $event->get_meta( 'new_key' ) ); - - // Delete all meta items. - $event->delete_meta(); - $this->assertEquals( array(), $event->get_meta() ); - - } - - /** - * Test meta getters/setters when the data is saved (ensure db serialization is working properly). - * - * @since 3.36.0 - * - * @return void - */ - public function test_meta_store() { - - $args = array( - 'actor_id' => 1, - 'object_type' => 'post', - 'object_id' => 1, - 'event_type' => 'page', - 'event_action' => 'load', - ); - - $meta = array( - 'meta_key' => 'meta_val', - 'another' => 1, - ); - - $event = new LLMS_Event(); - $event->setUp( $args )->save(); - - $event->set_metas( $meta, true ); - - $event = new LLMS_Event( $event->get( 'id' ), true ); - $this->assertEquals( wp_json_encode( $meta ), $event->get( 'meta' ) ); - $this->assertEquals( $meta, $event->get_meta() ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-instructor.php b/tests/phpunit/unit-tests/models/class-llms-test-instructor.php deleted file mode 100644 index 696786870d..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-instructor.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Test INsturctor model - * - * @package LifterLMS_Tests/Models - * - * @group LLMS_Instructor - * - * @since 3.34.0 - * @version 3.34.0 - */ -class LLMS_Test_Instructor extends LLMS_Unit_Test_Case { - - /** - * Test something - * - * @since 3.34.0 - * - * @return void - */ - public function test_has_student() { - - $instructor = $this->factory->instructor->create_and_get(); - $student = $this->factory->student->create_and_get(); - - $this->assertFalse( $instructor->has_student( $student ) ); - - $course_1 = $this->factory->course->create_and_get( array( 'sections' => 0 ) ); - $course_1->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) ); - - $course_2 = $this->factory->course->create_and_get( array( 'sections' => 0 ) ); - $course_2->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) ); - - $this->assertFalse( $instructor->has_student( 'fake' ) ); - $this->assertFalse( $instructor->has_student( $student ) ); - $this->assertFalse( $instructor->has_student( $student->get( 'id' ) ) ); - $this->assertFalse( $instructor->has_student( llms_get_student( $student ) ) ); - - $student->enroll( $course_2->get( 'id' ) ); - - $this->assertTrue( $instructor->has_student( $student ) ); - - $student->enroll( $course_1->get( 'id' ) ); - - $this->assertTrue( $instructor->has_student( $student ) ); - - $student->unenroll( $course_1->get( 'id' ) ); - $student->unenroll( $course_2->get( 'id' ) ); - - $this->assertFalse( $instructor->has_student( $student ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-access-plan.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-access-plan.php deleted file mode 100644 index a5ed990713..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-access-plan.php +++ /dev/null @@ -1,1213 +0,0 @@ -<?php -/** - * Tests for LifterLMS Coupon Model - * - * @package LifterLMS_Tests/Models - * - * @group access_plan - * - * @since 3.23.0 - * @since 3.30.1 Add tests for get_initial_price() method. - * @since 3.40.0 Improved tests for the `requires_payment()` method. - */ -class LLMS_Test_LLMS_Access_Plan extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_Access_Plan'; - - /** - * DB post type of the model being tested - * - * @var string - */ - protected $post_type = 'llms_access_plan'; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array - * - * @since 3.23.0 - * - * @return array - */ - protected function get_properties() { - return array( - 'access_expiration' => 'string', - 'access_expires' => 'string', - 'access_length' => 'absint', - 'access_period' => 'string', - 'availability' => 'string', - 'availability_restrictions' => 'array', - 'enroll_text' => 'string', - 'frequency' => 'absint', - 'is_free' => 'yesno', - 'length' => 'absint', - 'menu_order' => 'absint', - 'on_sale' => 'yesno', - 'period' => 'string', - 'price' => 'float', - 'product_id' => 'absint', - 'sale_end' => 'string', - 'sale_start' => 'string', - 'sale_price' => 'float', - 'sku' => 'string', - 'title' => 'string', - 'trial_length' => 'absint', - 'trial_offer' => 'yesno', - 'trial_period' => 'string', - 'trial_price' => 'float', - ); - } - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters - * - * @since 3.23.0 - * - * @return array - */ - protected function get_data() { - return array( - 'access_expiration' => 'lifetime', - 'access_expires' => '01/01/2018', - 'access_length' => 2, - 'access_period' => 'year', - 'availability' => 'open', // members - 'availability_restrictions' => array(), - 'enroll_text' => 'Enroll Now', - 'frequency' => 0, - 'is_free' => 'no', - 'length' => 0, - 'menu_order' => 0, - 'on_sale' => 'no', - 'period' => 'year', - 'price' => 25.99, - 'product_id' => $this->factory->post->create( array( - 'post_type' => 'course', - ) ), - 'sale_end' => '2018-02-03', - 'sale_start' => '2018-01-15', - 'sale_price' => 5.99, - 'sku' => 'testsku', - 'title' => 'Access Plan Title', - 'trial_length' => 1, - 'trial_offer' => 'no', - 'trial_period' => 'week', - 'trial_price' => 1.99, - ); - } - - /** - * Setup plan product association. - * - * @since 3.23.0 - * - * @param string $type Associated post type. - * - * @return void - */ - protected function set_obj_product( $type = 'course' ) { - $this->obj->set( 'product_id', $this->factory->post->create( array( - 'post_type' => $type, - ) ) ); - } - - /** - * Setup the test case - * - * @since 3.23.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->create(); - } - - /** - * Test can_expire() - * - * @since 3.23.0 - * - * @return void - */ - public function test_can_expire() { - - $opts = array( - '' => true, // @todo empty values should return false - 'fake' => true, // @todo fake values should return false - 'lifetime' => false, - 'limited-period' => true, - 'limited-date' => true, - ); - - foreach ( $opts as $val => $expect ) { - $this->obj->set( 'access_expiration', $val ); - $this->assertEquals( $expect, $this->obj->can_expire() ); - } - - } - - /** - * Override to prevent output of skipped test since the test doesn't matter for this class. - * - * @since 5.3.0 - * - * @return void - */ - public function test_edit_date() { - $this->assertTrue( true ); - } - - /** - * Test get_access_period_name() - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_access_period_name() { - - // Use values from the plan. - $this->obj->set( 'access_period', 'week' ); - $this->obj->set( 'access_length', 2 ); - $this->assertEquals( 'weeks', $this->obj->get_access_period_name() ); - - // Pass in values. - $this->assertEquals( 'day', $this->obj->get_access_period_name( 'day', 1 ) ); - $this->assertEquals( 'month', $this->obj->get_access_period_name( 'month', 1 ) ); - $this->assertEquals( 'years', $this->obj->get_access_period_name( 'years', 25 ) ); - - } - - /** - * Test get_checkout_url() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_checkout_url() { - - $this->set_obj_product(); - LLMS_Install::create_pages(); - - // no restrictions - $url = add_query_arg( 'plan', $this->obj->get( 'id' ), get_permalink( get_option( 'lifterlms_checkout_page_id' ) ) ); - $this->assertEquals( $url, $this->obj->get_checkout_url() ); - - // 1 restriction returns link to that membership - $membership_id = $this->factory->post->create( array( - 'post_type' => 'llms_membership', - ) ); - $this->obj->set( 'availability', 'members' ); - $this->obj->set( 'availability_restrictions', array( $membership_id ) ); - $this->assertEquals( get_permalink( $membership_id ), $this->obj->get_checkout_url() ); - - // multiple returns the hash for popover display - $this->obj->set( 'availability_restrictions', array( $membership_id, 1234 ) ); - $this->assertEquals( '#llms-plan-locked', $this->obj->get_checkout_url() ); - - // bypass availability checks - $this->assertEquals( $url, $this->obj->get_checkout_url( false ) ); - - } - - /** - * Test get_free_pricing_text() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_free_pricing_text() { - - $text = '<span class="lifterlms-price">FREE</span>'; - $this->assertEquals( $text, $this->obj->get_free_pricing_text() ); - $this->assertEquals( $text, $this->obj->get_free_pricing_text( 'html' ) ); - $this->assertEquals( 0.00, $this->obj->get_free_pricing_text( 'float' ) ); - - } - - /** - * Test the get_initial_price() method. - * - * @since 3.30.1 - * - * @return void - */ - public function test_get_initial_price() { - - // trial w/ no price - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - $this->assertSame( 0.00, $this->obj->get_initial_price() ); - - // free trial. - $this->obj->set( 'trial_price', 0 ); - $this->assertSame( 0.00, $this->obj->get_initial_price() ); - - // paid trial. - $this->obj->set( 'trial_price', 1 ); - $this->assertSame( 1.00, $this->obj->get_initial_price() ); - - - // disable the trial. - $this->obj->set( 'trial_offer', 'no' ); - - - // No sale price set. - $this->obj->set( 'on_sale', 'yes' ); - $this->assertSame( 0.00, $this->obj->get_initial_price() ); - - // on sale for free. - $this->obj->set( 'sale_price', 0 ); - $this->assertSame( 0.00, $this->obj->get_initial_price() ); - - // paid sale. - $this->obj->set( 'sale_price', 1 ); - $this->assertSame( 1.00, $this->obj->get_initial_price() ); - - - // disable the sale. - $this->obj->set( 'on_sale', 'no' ); - - - // free. - $this->obj->set( 'price', 0 ); - $this->assertSame( 0.00, $this->obj->get_initial_price() ); - - $this->obj->set( 'price', 2 ); - $this->assertSame( 2.00, $this->obj->get_initial_price() ); - - } - - /** - * Test the get_initial_price() method when using coupons. - * - * @since 3.30.1 - * - * @return void - */ - public function test_get_initial_price_with_coupon() { - - $coupon_id = $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ); - $coupon = llms_get_post( $coupon_id ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - $coupon->set( 'enable_trial_discount', 'yes' ); - $coupon->set( 'trial_amount', 100 ); - - - // Trial 100% discount. - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - $this->obj->set( 'trial_price', 1 ); - $this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id ) ); - - // Trial 50% discount. - $coupon->set( 'trial_amount', 50 ); - $this->assertSame( 0.50, $this->obj->get_initial_price( array(), $coupon_id ) ); - - // No trial offer. - $this->obj->set( 'trial_offer', 'no' ); - - // Free with coupon. - $this->obj->set( 'price', 10 ); - $this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id ) ); - - // 50% off coupon. - $coupon->set( 'coupon_amount', 50 ); - $this->assertSame( 5.00, $this->obj->get_initial_price( array(), $coupon_id ) ); - - // free with coupon. - $this->obj->set( 'is_free', 'yes' ); - $this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id) ); - - } - - /** - * Test get_price() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_price() { - - $prices = array( - 'price', - 'trial_price', - 'sale_price', - ); - - foreach ( $prices as $key ) { - - $this->obj->set( $key, 1.00 ); - $this->assertEquals( llms_price( 1.00 ), $this->obj->get_price( $key ) ); - $this->assertEquals( 1.00, $this->obj->get_price( $key, array(), 'float' ) ); - - $this->obj->set( $key, 0.00 ); - $this->assertEquals( $this->obj->get_free_pricing_text(), $this->obj->get_price( $key ) ); - $this->assertEquals( 0.00, $this->obj->get_price( $key, array(), 'float' ) ); - - } - - } - - // public function test_get_price_with_coupon() {} - - /** - * Test get_product() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_product() { - - $this->set_obj_product(); - $this->assertTrue( is_a( $this->obj->get_product(), 'LLMS_Product' ) ); - - } - - /** - * Test get_product_type() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_product_type() { - - $this->set_obj_product(); - $this->assertEquals( 'course', $this->obj->get_product_type() ); - - $this->set_obj_product( 'llms_membership' ); - $this->assertEquals( 'membership', $this->obj->get_product_type() ); - - } - - /** - * Test get_enroll_text() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_enroll_text() { - - // course - $this->set_obj_product(); - $this->assertEquals( 'Enroll', $this->obj->get_enroll_text() ); - - // membership - $this->set_obj_product( 'llms_membership' ); - $this->assertEquals( 'Join', $this->obj->get_enroll_text() ); - - // custom - $this->obj->set( 'enroll_text', 'DO SOMETHING!' ); - $this->assertEquals( 'DO SOMETHING!', $this->obj->get_enroll_text() ); - - } - - /** - * Test get_expiration_details() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_expiration_details() { - - $this->assertEquals( '', $this->obj->get_expiration_details() ); - - $this->obj->set( 'access_expiration', 'limited-date' ); - $this->assertTrue( 0 === strpos( $this->obj->get_expiration_details(), 'access until' ) ); - - $this->obj->set( 'access_expiration', 'limited-period' ); - $this->assertTrue( false !== strpos( $this->obj->get_expiration_details(), 'of access' ) ); - - } - - /** - * Test get_schedule_details() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_schedule_details() { - - $this->assertEquals( '', $this->obj->get_schedule_details() ); - - $this->obj->set( 'period', 'week' ); - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'length', 0 ); - - $this->assertEquals( 'per week', $this->obj->get_schedule_details() ); - $this->assertTrue( false === strpos( $this->obj->get_schedule_details(), 'total payments' ) ); - - $this->obj->set( 'frequency', 2 ); - $this->assertEquals( 'every 2 weeks', $this->obj->get_schedule_details() ); - - - $this->obj->set( 'length', 3 ); - $this->assertEquals( 'every 2 weeks for 3 total payments', $this->obj->get_schedule_details() ); - - } - - /** - * Test get_trial_details() - * - * @since 3.23.0 - * - * @return void - */ - public function test_get_trial_details() { - - $this->assertEquals( '', $this->obj->get_trial_details() ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - $this->obj->set( 'trial_length', 1 ); - $this->obj->set( 'trial_period', 'year' ); - - $this->assertEquals( 'for 1 year', $this->obj->get_trial_details() ); - - } - - /** - * Test visibility getters / setters - * - * @since 3.23.0 - * - * @return void - */ - public function test_visibility() { - - $this->assertEquals( 'visible', $this->obj->get_visibility() ); - - $opts = array( - 'visible' => array( - 'is_featured' => false, - 'is_visible' => true, - ), - 'hidden' => array( - 'is_featured' => false, - 'is_visible' => false, - ), - 'featured' => array( - 'is_featured' => true, - 'is_visible' => true, - ), - ); - - foreach ( $opts as $opt => $tests ) { - $this->obj->set_visibility( $opt ); - $this->assertEquals( $opt, $this->obj->get_visibility() ); - foreach ( $tests as $func => $expect ) { - $this->assertEquals( $expect, call_user_func( array( $this->obj, $func ) ) ); - } - } - - } - - /** - * Test has_availability_restrictions() - * - * @since 3.23.0 - * - * @return void - */ - public function test_has_availability_restrictions() { - - $this->set_obj_product( 'llms_membership' ); - $this->assertFalse( $this->obj->has_availability_restrictions() ); - - $this->set_obj_product(); - $this->assertFalse( $this->obj->has_availability_restrictions() ); - - $this->obj->set( 'availability', 'members' ); - $this->assertFalse( $this->obj->has_availability_restrictions() ); - - $this->obj->set( 'availability_restrictions', array( 12345 ) ); - $this->assertTrue( $this->obj->has_availability_restrictions() ); - - } - - /** - * Test has_free_checkout() - * - * @since 3.23.0 - * - * @return void - */ - public function test_has_free_checkout() { - - $this->assertFalse( $this->obj->has_free_checkout() ); - - $this->obj->set( 'is_free', 'no' ); - $this->assertFalse( $this->obj->has_free_checkout() ); - - $this->obj->set( 'is_free', 'fake' ); - $this->assertFalse( $this->obj->has_free_checkout() ); - - $this->obj->set( 'is_free', '' ); - $this->assertFalse( $this->obj->has_free_checkout() ); - - $this->obj->set( 'is_free', 'yes' ); - $this->assertTrue( $this->obj->has_free_checkout() ); - - } - - /** - * Test has_trial() - * - * @since 3.23.0 - * - * @return void - */ - public function test_has_trial() { - - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'frequency', 0 ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'frequency', 1 ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'frequency', 1 ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'trial_offer', 'no' ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'trial_offer', 'yes' ); - $this->assertTrue( $this->obj->has_trial() ); - - } - - /** - * Test is_available_to_user() - * - * @since 3.23.0 - * - * @return void - */ - public function test_is_available_to_user() { - - $this->set_obj_product(); - $this->assertTrue( $this->obj->is_available_to_user() ); - - $mid = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ); - - $this->obj->set( 'availability', 'members' ); - $this->obj->set( 'availability_restrictions', array( $mid ) ); - - $this->assertFalse( $this->obj->is_available_to_user() ); - - // enroll the student - $uid = $this->factory->user->create(); - llms_enroll_student( $uid, $mid ); - $this->assertTrue( $this->obj->is_available_to_user( $uid ) ); - - } - - /** - * Test is_free() - * - * @since 3.23.0 - * - * @return void - */ - public function test_is_free() { - - $this->assertFalse( $this->obj->is_free() ); - - $this->obj->set( 'is_free', 'no' ); - $this->assertFalse( $this->obj->is_free() ); - - $this->obj->set( 'is_free', 'fake' ); - $this->assertFalse( $this->obj->is_free() ); - - $this->obj->set( 'is_free', '' ); - $this->assertFalse( $this->obj->is_free() ); - - $this->obj->set( 'is_free', 'yes' ); - $this->assertTrue( $this->obj->is_free() ); - - } - - /** - * Test is_on_sale() - * - * @since 3.23.0 - * - * @return void - */ - public function test_is_on_sale() { - - // no vals, not on sale - $this->assertFalse( $this->obj->is_on_sale() ); - - $now = current_time( 'timestamp' ); - $future = date( 'Y-m-d', strtotime( '+1 year', $now ) ); - $past = date( 'Y-m-d', strtotime( '-1 year', $now ) ); - $now = date( 'Y-m-d', $now ); - - // on sale, no dates - $this->obj->set( 'on_sale', 'yes' ); - $this->assertTrue( $this->obj->is_on_sale() ); - - // start & end - $this->obj->set( 'sale_start', $past ); - $this->obj->set( 'sale_end', $future ); - $this->assertTrue( $this->obj->is_on_sale() ); - - // no start & has end - $this->obj->set( 'sale_start', '' ); - $this->assertTrue( $this->obj->is_on_sale() ); - - // has start & no end - $this->obj->set( 'sale_start', $past ); - $this->obj->set( 'sale_end', '' ); - $this->assertTrue( $this->obj->is_on_sale() ); - - // not on sale - $this->obj->set( 'on_sale', 'no' ); - $this->assertFalse( $this->obj->is_on_sale() ); - - // start in future - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'sale_start', $future ); - $this->obj->set( 'sale_end', '' ); - $this->assertFalse( $this->obj->is_on_sale() ); - - // end in past - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'sale_start', '' ); - $this->obj->set( 'sale_end', $past ); - $this->assertFalse( $this->obj->is_on_sale() ); - - // test on sale end at 00:00 of $future day plus 1 - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'sale_end', $future ); - - // set current current time as last second of $future day - llms_tests_mock_current_time( strtotime( $future . ' 23:59:59' ) ); - $this->assertTrue( $this->obj->is_on_sale() ); - - // set current current time as first second of $future day plus 1 - llms_tests_mock_current_time( strtotime( '+1 day', strtotime( $future . ' 00:00:00' ) ) ); - $this->assertFalse( $this->obj->is_on_sale() ); - - } - - /** - * Test is_recurring() - * - * @since 3.23.0 - * - * @return void - */ - public function test_is_recurring() { - - $this->assertFalse( $this->obj->is_recurring() ); - - $this->obj->set( 'frequency', 0 ); - $this->assertFalse( $this->obj->is_recurring() ); - - $this->obj->set( 'frequency', 1 ); - $this->assertTrue( $this->obj->is_recurring() ); - - $this->obj->set( 'frequency', 3 ); - $this->assertTrue( $this->obj->is_recurring() ); - - } - - /** - * Test requires_payment(): free plan - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_free() { - - $this->obj->set( 'is_free', 'yes' ); - $this->assertFalse( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): one-time payment - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time() { - - $this->obj->set( 'price', 1 ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): one-time payment with a paid sale - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_sale() { - - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): one-time payment with a free sale - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_sale_free() { - - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 0 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): one-time payment with a sale and a coupon - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_sale_coupon() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): one-time payment with a sale and a coupon that discounts price to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_sale_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): one-time payment with a coupon - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_coupon() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'price', 2 ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): one-time payment with a coupon that discounts the price to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_one_time_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'price', 2 ); - - $this->assertFalse( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 1 ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with sale - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_sale() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with sale reducing price to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_sale_free() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 0 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with sale and coupon - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_sale_coupon() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with sale and coupon reducing price to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_sale_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with paid trial - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with free trial - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_free() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 0 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon discounting recurring price to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon_trial_coupon_discount() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - $coupon->set( 'enable_trial_discount', 'yes' ); - $coupon->set( 'trial_amount', 50 ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon_free_trial_coupon_discount() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - $coupon->set( 'enable_trial_discount', 'yes' ); - $coupon->set( 'trial_amount', 50 ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon_trial_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 50 ); - $coupon->set( 'discount_type', 'percent' ); - $coupon->set( 'enable_trial_discount', 'yes' ); - $coupon->set( 'trial_amount', 100 ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_coupon_free_trial_coupon_free() { - - $coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) ); - $coupon->set( 'coupon_amount', 100 ); - $coupon->set( 'discount_type', 'percent' ); - $coupon->set( 'enable_trial_discount', 'yes' ); - $coupon->set( 'trial_amount', 100 ); - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment( $coupon ) ); - - } - - /** - * Test requires_payment(): recurring payment with paid trial and sale - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_sale() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 1 ); - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with paid trial and sale reducing recurring payment to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_sale_free() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 0 ); - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'trial_price', 1 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertTrue( $this->obj->requires_payment() ); - - } - - /** - * Test requires_payment(): recurring payment with free trial and sale reducing recurring payment to free - * - * @since 3.40.0 - * - * @return void - */ - public function test_requires_payment_recurring_trial_free_sale_free() { - - $this->obj->set( 'frequency', 1 ); - $this->obj->set( 'price', 2 ); - $this->obj->set( 'sale_price', 0 ); - $this->obj->set( 'on_sale', 'yes' ); - $this->obj->set( 'trial_price', 0 ); - $this->obj->set( 'trial_offer', 'yes' ); - - $this->assertFalse( $this->obj->requires_payment() ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-add-on.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-add-on.php deleted file mode 100644 index 06a59c617f..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-add-on.php +++ /dev/null @@ -1,356 +0,0 @@ -<?php -/** - * Test Add On model - * - * @package LifterLMS_Tests/Models - * - * @group LLMS_Add_On - * @group add_ons - * - * @since 4.21.3 - */ -class LLMS_Test_Add_On extends LLMS_Unit_Test_Case { - - /** - * Retrieve a mock plugin add-on for testing - * - * @since 5.1.1 - * - * @param boolean $install If true, calls `install_mock_addon()` to physically install the mock plugin. - * @param boolean $activate If true and `$install` is also true, activates the mock plugin following installation. - * @return LLMS_Add_On - */ - private function get_mock_addon( $install = false, $activate = false ) { - - $asset = 'lifterlms-mock-addon.php'; - $dir = 'lifterlms-mock-addon/'; - $file = $dir . $asset; - if ( $install ) { - LLMS_Unit_Test_Files::copy_asset( $asset, trailingslashit( WP_PLUGIN_DIR ). $dir ); - if ( $activate ) { - activate_plugin( $file ); - } - } - - return new LLMS_Add_On( array( - 'title' => 'LLMS Mock Add-on', - 'update_file' => $file, - 'id' => 'lifterlms-com-mock-addon', - 'type' => 'plugin', - ) ); - - } - - /** - * Test constructor with an addon array passed in. - * - * @since 4.21.3 - * - * @return void - */ - public function test_constructor_with_addon() { - - $mock = array( - 'id' => 'test', - 'key' => 'val', - ); - $addon = new LLMS_Add_On( $mock ); - - $this->assertEquals( $mock, LLMS_Unit_Test_Util::get_private_property_value( $addon, 'data' ) ); - $this->assertEquals( 'test', LLMS_Unit_Test_Util::get_private_property_value( $addon, 'id' ) ); - - } - - /** - * Test constructor with a lookup - * - * @since 4.21.3 - * - * @return void - */ - public function test_constructor_with_lookup() { - - $addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' ); - - $this->assertEquals( 'lifterlms-com-lifterlms', $addon->get( 'id' ) ); - $this->assertEquals( 'lifterlms-com-lifterlms', LLMS_Unit_Test_Util::get_private_property_value( $addon, 'id' ) ); - - } - - public function test_get() { - - $addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' ); - - // Non-existent prop. - $this->assertSame( '', $addon->get( 'fake' ) ); - - // Real prop. - $this->assertSame( 'LifterLMS', $addon->get( 'title' ) ); - - } - - /** - * Test plugin activation and deactivation - * - * Also tests the `is_active()` and partially the `get_status()` methods. - * - * @since 4.21.3 - * - * @return void - */ - public function test_activate_deactivate_plugin() { - - $addon = new LLMS_Add_On( array( 'title' => 'Akismet', 'type' => 'plugin', 'update_file' => 'akismet/akismet.php' ) ); - $activate = $addon->activate(); - $this->assertEquals( $activate, 'Akismet was successfully activated.' ); - - $this->assertTrue( $addon->is_active() ); - $this->assertEquals( 'active', $addon->get_status() ); - $this->assertEquals( 'Active', $addon->get_status( true ) ); - - $deactivate = $addon->deactivate(); - $this->assertEquals( $deactivate, 'Akismet was successfully deactivated.' ); - - $this->assertFalse( $addon->is_active() ); - $this->assertEquals( 'inactive', $addon->get_status() ); - $this->assertEquals( 'Inactive', $addon->get_status( true ) ); - - } - - /** - * Test theme activation - * - * Also tests the `is_active()` and partially the `get_status()` methods. - * - * @since 4.21.3 - * - * @return void - */ - public function test_activate_theme_success() { - - $addon = new LLMS_Add_On( array( 'title' => 'Default Theme', 'type' => 'theme', 'update_file' => 'twentynineteen' ) ); - - $this->assertFalse( $addon->is_active() ); - $this->assertEquals( 'inactive', $addon->get_status() ); - $this->assertEquals( 'Inactive', $addon->get_status( true ) ); - - $res = $addon->activate(); - $this->assertEquals( $res, 'Default Theme was successfully activated.' ); - - $this->assertTrue( $addon->is_active() ); - $this->assertEquals( 'active', $addon->get_status() ); - $this->assertEquals( 'Active', $addon->get_status( true ) ); - - } - - /** - * Test activate() error for a plugin - * - * @since 4.21.3 - * - * @return void - */ - public function test_activate_error() { - - $addon = new LLMS_Add_On( array( 'title' => 'fake', 'type' => 'plugin' ) ); - $activate = $addon->activate(); - - $this->assertIsWPError( $activate); - $this->assertWPErrorCodeEquals( 'activation', $activate ); - - } - - /** - * Test deactivate() error for a plugin - * - * @since 4.21.3 - * - * @return void - */ - public function test_deactivate_error() { - - $addon = new LLMS_Add_On( array( 'title' => 'fake' ) ); - $deactivate = $addon->deactivate(); - $this->assertIsWPError( $deactivate ); - $this->assertWPErrorCodeEquals( 'deactivation', $deactivate ); - - } - - /** - * Test get_channel_subscription() - * - * @since 4.21.3 - * - * @return void - */ - public function test_get_channel_subscription() { - - $addon = new LLMS_Add_On(); - $this->assertEquals( 'stable', $addon->get_channel_subscription() ); - - } - - /** - * Test get_type() - * - * @since 4.21.3 - * - * @return void - */ - public function test_get_type() { - - $tests = array( - 'theme' => array( 'type' => 'theme' ), - 'plugin' => array( 'type' => 'plugin' ), - 'fake' => array( 'type' => 'fake' ), - 'bundle' => array( 'categories' => array( 'bundles' => 'Bundles' ) ), - 'external' => array( 'categories' => array( 'third-party' => 'Third Party' ) ), - 'support' => array( 'categories' => array() ), - ); - - foreach ( $tests as $expected => $data ) { - $addon = new LLMS_Add_On( $data ); - $this->assertEquals( $expected, $addon->get_type(), $expected ); - } - - } - - /** - * Test get_permalink() - * - * @since 4.21.3 - * - * @return void - */ - public function test_get_permalink() { - - $addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' ); - $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; - $this->assertEquals( $expect, $addon->get_permalink() ); - - } - - /** - * Test get_install_status() and is_installed() - * - * @since 4.21.3 - * - * @return void - */ - public function test_install_status() { - - // Invalid. - $addon = new LLMS_Add_On(); - $this->assertEquals( 'none', $addon->get_install_status() ); - $this->assertEquals( 'N/A', $addon->get_install_status( true ) ); - - // Plugin installed. - $addon = new LLMS_Add_On( array( 'type' => 'plugin', 'update_file' => 'akismet/akismet.php' ) ); - $this->assertEquals( 'installed', $addon->get_install_status() ); - $this->assertEquals( 'Installed', $addon->get_install_status( true ) ); - - // Plugin not installed. - $addon = new LLMS_Add_On( array( 'type' => 'plugin', 'update_file' => 'mock/mock.php' ) ); - $this->assertEquals( 'uninstalled', $addon->get_install_status() ); - $this->assertEquals( 'Not Installed', $addon->get_install_status( true ) ); - - // Theme installed. - $addon = new LLMS_Add_On( array( 'type' => 'theme', 'update_file' => 'twentynineteen' ) ); - $this->assertEquals( 'installed', $addon->get_install_status() ); - $this->assertEquals( 'Installed', $addon->get_install_status( true ) ); - - // Theme not installed. - $addon = new LLMS_Add_On( array( 'type' => 'theme', 'update_file' => 'fake' ) ); - $this->assertEquals( 'uninstalled', $addon->get_install_status() ); - $this->assertEquals( 'Not Installed', $addon->get_install_status( true ) ); - - } - - /** - * Test lookup_add_on() when errors are encountered. - * - * @since 4.21.3 - * - * @return void - */ - public function test_lookup_errors() { - - $addon = new LLMS_Add_On(); - - // Mock the HTTP request to find addons for an error. - $err = new WP_Error( 'mocked-err', 'Mocked Message', array( 'data' => 'mocked' ) ); - $this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $err ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $addon, 'lookup_add_on', array( 'mock', 'mock' ) ) ); - - // Mock the HTTP request to return an empty array for some reason.. - $ret = array( 'items' => array() ); - $this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $ret ); - - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $addon, 'lookup_add_on', array( 'mock', 'mock' ) ) ); - - } - - /** - * Test uninstall() for an add-on that isn't installed - * - * @since 5.1.1 - * - * @return void - */ - public function test_uninstall_error_addon_not_installed() { - - $addon = llms_get_add_on( 'lifterlms-groups', 'slug' ); - $res = $addon->uninstall(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'not-installed', $res ); - - } - - /** - * Test uninstall() error for an active add-on. - * - * @since 5.1.1 - * - * @return void - */ - public function test_uninstall_error_is_activate() { - - $addon = $this->get_mock_addon( true, true ); - $res = $addon->uninstall(); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'uninstall-active', $res ); - - } - - /** - * Test uninstall() error for an invalid add-on type. - * - * @since 5.1.1 - * - * @return void - */ - public function test_uninstall_real_error_invalid_type() { - - $addon = new LLMS_Add_On( array( 'type' => 'fake' ) ); - $res = LLMS_Unit_Test_Util::call_method( $addon, 'uninstall_real' ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'uninstall-invalid-type', $res ); - - } - - /** - * Test uninstall() success for a plugin add-on - * - * @since 5.1.1 - * - * @return void - */ - public function test_uninstall_plugin_real_success() { - - $addon = $this->get_mock_addon( true, false ); - $res = LLMS_Unit_Test_Util::call_method( $addon, 'uninstall_real' ); - $this->assertEquals( 'LLMS Mock Add-on was successfully uninstalled.', $res ); - - } -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-coupon.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-coupon.php deleted file mode 100644 index 86f420a8f1..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-coupon.php +++ /dev/null @@ -1,211 +0,0 @@ -<?php -/** - * Tests for LifterLMS Coupon Model - * @group coupons - * @since 3.4.0 - * @version 3.19.0 - */ -class LLMS_Test_LLMS_Coupon extends LLMS_PostModelUnitTestCase { - - /** - * class name for the model being tested by the class - * @var string - */ - protected $class_name = 'LLMS_Coupon'; - - /** - * db post type of the model being tested - * @var string - */ - protected $post_type = 'llms_coupon'; - - /** - * Get properties, used by test_getters_setters - * This should match, exactly, the object's $properties array - * @return array - * @since 3.4.0 - * @version 3.4.0 - */ - protected function get_properties() { - return array( - 'coupon_amount' => 'float', - 'coupon_courses' => 'array', - 'coupon_membership' => 'array', - 'description' => 'string', - 'discount_type' => 'string', - 'enable_trial_discount' => 'yesno', - 'expiration_date' => 'string', - 'plan_type' => 'string', - 'trial_amount' => 'float', - 'usage_limit' => 'absint', - ); - } - - /** - * Get data to fill a create post with - * This is used by test_getters_setters - * @return array - * @since 3.4.0 - * @version 3.4.0 - */ - protected function get_data() { - return array( - 'coupon_amount' => 50, - 'coupon_courses' => array(), - 'coupon_membership' => array(), - 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'discount_type' => 'percent', - 'enable_trial_discount' => 'no', - 'expiration_date' => '02/17/2017', - 'plan_type' => 'any', - 'trial_amount' => 5, - 'usage_limit' => 25, - ); - } - - - /* - /$$ /$$ - | $$ | $$ - /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$ - |_ $$_/ /$$__ $$ /$$_____/|_ $$_/ /$$_____/ - | $$ | $$$$$$$$| $$$$$$ | $$ | $$$$$$ - | $$ /$$| $$_____/ \____ $$ | $$ /$$\____ $$ - | $$$$/| $$$$$$$ /$$$$$$$/ | $$$$//$$$$$$$/ - \___/ \_______/|_______/ \___/ |_______/ - */ - - /** - * Test the get expiration time function - * @return void - * @since 3.19.0 - * @version 3.19.0 - */ - public function test_get_expiration_time() { - - $this->create(); - - // no expiration date - $this->obj->set( 'expiration_date', '' ); - $this->assertFalse( $this->obj->get_expiration_time() ); - - $dates = array( - '02/28/2018', - '01/31/2015', - '12/31/2016', - '05/05/2015', - ); - - foreach ( $dates as $date ) { - $this->obj->set( 'expiration_date', $date ); - $this->assertEquals( ( strtotime( $date ) + DAY_IN_SECONDS - 1 ), $this->obj->get_expiration_time() ); - } - - - } - - /** - * Test get_products() function - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - public function test_get_products() { - $this->create(); - $this->obj->set( 'coupon_courses', array( 1, 2, 3 ) ); - $this->obj->set( 'coupon_membership', array( 4, 5, 6 ) ); - $this->assertEquals( array( 1, 2, 3, 4, 5, 6 ), $this->obj->get_products() ); - } - - /** - * test the has_main_discount() method - * @return void - * @since 3.21.1 - * @version 3.21.1 - */ - public function test_has_main_discount() { - - $this->create(); - - // not set - $this->assertFalse( $this->obj->has_main_discount() ); - - // set to various positive numbers - $amounts = array( - '1', 1, '1.00', 1.00, 200, 2934234, 234.32, 0.50, '0.99' - ); - foreach ( $amounts as $amount ) { - $this->obj->set( 'coupon_amount', $amount ); - $this->assertTrue( $this->obj->has_main_discount() ); - } - - // 0 amounts - $amounts = array( - 0, false, '', '0', '0.00', null, 0.00, '.00', 'no', 'arst', - ); - foreach ( $amounts as $amount ) { - $this->obj->set( 'coupon_amount', $amount ); - $this->assertFalse( $this->obj->has_main_discount() ); - } - - } - - /** - * Test has_trial_discount() function - * @return void - * @since 3.4.0 - * @version 3.4.0 - */ - public function test_has_trial_discount() { - - $this->create(); - - // trial discount enabled - $this->obj->set( 'enable_trial_discount', 'yes' ); - $this->assertTrue( $this->obj->has_trial_discount() ); - - // trial discount not enabled - $this->obj->set( 'enable_trial_discount', 'no' ); - $this->assertFalse( $this->obj->has_trial_discount() ); - $this->obj->set( 'enable_trial_discount', '' ); - $this->assertFalse( $this->obj->has_trial_discount() ); - $this->obj->set( 'enable_trial_discount', 'string' ); - $this->assertFalse( $this->obj->has_trial_discount() ); - - } - - /** - * Test is_expired function - * @return void - * @since 3.2.2 - * @version 3.19.0 - */ - public function test_is_expired() { - - $this->create(); - - // no date set so it's not expired - $this->assertFalse( $this->obj->is_expired() ); - - // date empty, not expired - $this->obj->set( 'expiration_date', '' ); - $this->assertFalse( $this->obj->is_expired() ); - - // should be expired - llms_mock_current_time( '2016-01-02' ); - $this->obj->set( 'expiration_date', '01/01/2016' ); - $this->assertTrue( $this->obj->is_expired() ); - - // should not be expired - llms_mock_current_time( '2015-01-01' ); - $this->obj->set( 'expiration_date', '01/01/2016' ); - $this->assertFalse( $this->obj->is_expired() ); - - // should expire end of day on expiration date - llms_mock_current_time( '2016-01-01 12:00pm' ); - $this->obj->set( 'expiration_date', '01/01/2016' ); - $this->assertFalse( $this->obj->is_expired() ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-course.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-course.php deleted file mode 100644 index 276f177226..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-course.php +++ /dev/null @@ -1,636 +0,0 @@ -<?php -/** - * Tests for LifterLMS Course Model - * - * @group LLMS_Course - * @group LLMS_Post_Model - * - * @since 3.4.0 - * @since 3.24.0 Add tests for the `get_available_points()` method. - * @since 4.7.0 Add tests for `to_array_extra_blocks()` and `to_array_extra_images()`. - * @since 5.2.1 Add checks for empty URL and page ID in `test_has_sales_page_redirect()`. - */ -class LLMS_Test_LLMS_Course extends LLMS_PostModelUnitTestCase { - - /** - * class name for the model being tested by the class - * @var string - */ - protected $class_name = 'LLMS_Course'; - - /** - * db post type of the model being tested - * @var string - */ - protected $post_type = 'course'; - - /** - * Get properties, used by test_getters_setters - * This should match, exactly, the object's $properties array - * - * @since 3.4.0 - * @since 3.20.0 Unknown. - * @since 4.12.0 Added missing values. - * - * @return array - */ - protected function get_properties() { - return array( - // Public. - 'audio_embed' => 'text', - 'average_grade' => 'float', - 'average_progress' => 'float', - 'capacity' => 'absint', - 'capacity_message' => 'text', - 'course_closed_message' => 'text', - 'course_opens_message' => 'text', - 'content_restricted_message' => 'text', - 'enable_capacity' => 'yesno', - 'end_date' => 'text', - 'enrolled_students' => 'absint', - 'enrollment_closed_message' => 'text', - 'enrollment_end_date' => 'text', - 'enrollment_opens_message' => 'text', - 'enrollment_period' => 'yesno', - 'enrollment_start_date' => 'text', - 'has_prerequisite' => 'yesno', - 'length' => 'text', - 'prerequisite' => 'absint', - 'prerequisite_track' => 'absint', - 'sales_page_content_page_id' => 'absint', - 'sales_page_content_type' => 'string', - 'sales_page_content_url' => 'string', - 'tile_featured_video' => 'yesno', - 'time_period' => 'yesno', - 'start_date' => 'text', - 'video_embed' => 'text', - ); - } - - /** - * Get data to fill a create post with - * This is used by test_getters_setters - * @return array - * @since 3.4.0 - * @version 3.20.0 - */ - protected function get_data() { - return array( - 'audio_embed' => 'http://example.tld/audio_embed', - 'average_grade' => 25.55, - 'average_progress' => 99.32, - 'capacity' => 25, - 'capacity_message' => 'Capacity Reached', - 'course_closed_message' => 'Course has closed', - 'course_opens_message' => 'Course is not yet open', - 'content_restricted_message' => 'You cannot access this content', - 'enable_capacity' => 'yes', - 'end_date' => '2017-05-05', - 'enrolled_students' => 25, - 'enrollment_closed_message' => 'Enrollment is closed', - 'enrollment_end_date' => '2017-05-05', - 'enrollment_opens_message' => 'Enrollment opens later', - 'enrollment_period' => 'yes', - 'enrollment_start_date' => '2017-05-01', - 'has_prerequisite' => 'no', - 'length' => '1 year', - 'prerequisite' => 0, - 'prerequisite_track' => 0, - 'tile_featured_video' => 'yes', - 'time_period' => 'yes', - 'sales_page_content_page_id' => 0, - 'sales_page_content_type' => 'none', - 'sales_page_content_url' => 'https://lifterlms.com', - 'start_date' => '2017-05-01', - 'video_embed' => 'http://example.tld/video_embed', - ); - } - - /** - * Test the get_available_points() method - * @return [type] - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_available_points() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 0, 0 )[0] ); - - // default setup is 1 point per lesson - $this->assertEquals( 10, $course->get_available_points() ); - - // change them all up - $points = 0; - foreach ( $course->get_lessons() as $lesson ) { - $lesson_points = rand( 0, 3 ); - $lesson->set( 'points', $lesson_points ); - $points += $lesson_points; - } - $this->assertEquals( $points, $course->get_available_points() ); - - } - - /** - * Test Audio and Video Embeds - * - * @since 3.4.0 - * @since 4.10.0 Fix faulty tests, use assertSame in favor of assertEquals. - * - * @return void - */ - public function test_get_embeds() { - - $course = new LLMS_Course( 'new', 'Course With Embeds' ); - - $audio_url = 'https://open.spotify.com/track/1rNUOtuCWv1qswqsMFvzvz'; - $video_url = 'https://www.youtube.com/watch?v=MhQlNwxn5oo'; - - // Empty string when none set. - $this->assertEmpty( $course->get_audio() ); - $this->assertEmpty( $course->get_video() ); - - $course->set( 'audio_embed', $audio_url ); - $course->set( 'video_embed', $video_url ); - - $audio_embed = $course->get_audio(); - $video_embed = $course->get_video(); - - // Should be an iframe for valid embeds. - $this->assertSame( 0, strpos( $audio_embed, '<iframe' ) ); - $this->assertSame( 0, strpos( $video_embed, '<iframe' ) ); - - // Fallbacks should be a link to the URL. - $not_embeddable_url = 'http://lifterlms.com/not/embeddable'; - - $course->set( 'audio_embed', $not_embeddable_url ); - $course->set( 'video_embed', $not_embeddable_url ); - $audio_embed = $course->get_audio(); - $video_embed = $course->get_video(); - - $this->assertSame( 0, strpos( $audio_embed, '<a' ) ); - $this->assertSame( 0, strpos( $video_embed, '<a' ) ); - - $this->assertStringContains( sprintf( 'href="%s"', $not_embeddable_url ), $audio_embed ); - $this->assertStringContains( sprintf( 'href="%s"', $not_embeddable_url ), $video_embed ); - - } - - /** - * Test get percent complete from course - * @return void - * @since 3.17.2 - * @version 3.17.2 - */ - public function test_get_percent_complete() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 4, 4, 0, 0 )[0] ); - $student = $this->get_mock_student(); - - $student->enroll( $course->get( 'id' ) ); - - // get student by ID - $this->assertEquals( 0, $course->get_percent_complete( $student->get( 'id' ) ) ); - - // get from current user - $this->assertEquals( 0, $course->get_percent_complete() ); - - // complete some courses - $this->complete_courses_for_student( $student->get_id(), $course->get( 'id' ), 75 ); - - // get by id - $this->assertEquals( 75, $course->get_percent_complete( $student->get( 'id' ) ) ); - - // get from current user - $this->assertEquals( 0, $course->get_percent_complete() ); - - // log the user in - wp_set_current_user( $student->get_id() ); - - // get from current user - $this->assertEquals( 75, $course->get_percent_complete() ); - - - } - - /** - * Test prerequisite functions related to courses - * @return void - * @since 3.4.0 - * @version 3.7.3 - */ - public function test_get_prerequisites() { - - $course = new LLMS_Course( 'new', 'Course Name' ); - $prereq_course = new LLMS_Course( 'new', 'Course Prereq' ); - $prereq_track = wp_create_term( 'test track', 'course_track' ); - - // no prereqs - $this->assertFalse( $course->has_prerequisite( 'any' ) ); - $this->assertFalse( $course->has_prerequisite( 'course' ) ); - $this->assertFalse( $course->has_prerequisite( 'course_track' ) ); - $this->assertFalse( $course->get_prerequisite_id( 'course' ) ); - $this->assertFalse( $course->get_prerequisite_id( 'course_track' ) ); - - $course->set( 'prerequisite', $prereq_course->get( 'id' ) ); - $course->set( 'prerequisite_track', $prereq_track['term_id'] ); - - // still no prereqs - $this->assertFalse( $course->has_prerequisite( 'any' ) ); - $this->assertFalse( $course->has_prerequisite( 'course' ) ); - $this->assertFalse( $course->has_prerequisite( 'course_track' ) ); - $this->assertFalse( $course->get_prerequisite_id( 'course' ) ); - $this->assertFalse( $course->get_prerequisite_id( 'course_track' ) ); - - $course->set( 'has_prerequisite', 'yes' ); - - // have prereqs - $this->assertTrue( $course->has_prerequisite( 'any' ) ); - $this->assertTrue( $course->has_prerequisite( 'course' ) ); - $this->assertTrue( $course->has_prerequisite( 'course_track' ) ); - $this->assertEquals( $prereq_course->get( 'id' ), $course->get_prerequisite_id( 'course' ) ); - $this->assertEquals( $prereq_track['term_id'], $course->get_prerequisite_id( 'course_track' ) ); - - $course->set( 'prerequisite', 0 ); - - $this->assertTrue( $course->has_prerequisite( 'any' ) ); - $this->assertFalse( $course->has_prerequisite( 'course' ) ); - $this->assertTrue( $course->has_prerequisite( 'course_track' ) ); - $this->assertEquals( 0, $course->get_prerequisite_id( 'course' ) ); - - $course->set( 'prerequisite', 'string' ); - $this->assertFalse( $course->has_prerequisite( 'course' ) ); - $this->assertEquals( 0, $course->get_prerequisite_id( 'course' ) ); - - } - - /** - * Test the get lessons function - * @return void - * @since 3.12.0 - * @version 3.12.0 - */ - public function test_get_lessons() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 2, 2, 0, 0 )[0] ); - - // get just ids - $lessons = $course->get_lessons( 'ids' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $id ) { - $this->assertTrue( is_numeric( $id ) ); - }, $lessons ); - - // wp post objects - $lessons = $course->get_lessons( 'posts' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $post ) { - $this->assertTrue( is_a( $post, 'WP_Post' ) ); - }, $lessons ); - - // lesson objects - $lessons = $course->get_lessons( 'lessons' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $lesson ) { - $this->assertTrue( is_a( $lesson, 'LLMS_Lesson' ) ); - }, $lessons ); - - } - - /** - * Test the get quizzes function - * @return void - * @since 3.12.0 - * @version 3.12.0 - */ - public function test_get_quizzes() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 3, 1 )[0] ); - - $quizzes = $course->get_quizzes(); - $this->assertEquals( 3, count( $quizzes ) ); - array_map( function( $id ) { - $this->assertTrue( is_numeric( $id ) ); - }, $quizzes ); - - } - - /** - * Test get_sales_page_url method - * @return void - * @since 3.20.0 - * @version 3.20.0 - */ - public function test_get_sales_page_url() { - - $course = new LLMS_Course( 'new', 'Course Name' ); - - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'none' ); - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'content' ); - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'url' ); - $course->set( 'sales_page_content_url', 'https://lifterlms.com' ); - $this->assertEquals( 'https://lifterlms.com', $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'page' ); - $page = $this->factory->post->create(); - $course->set( 'sales_page_content_page_id', $page ); - $this->assertEquals( get_permalink( $page ), $course->get_sales_page_url() ); - - } - - /** - * Test the get sections function - * @return void - * @since 3.12.0 - * @version 3.12.0 - */ - public function test_get_sections() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 4, 0, 0, 0 )[0] ); - - // get just ids - $sections = $course->get_sections( 'ids' ); - $this->assertEquals( 4, count( $sections ) ); - array_map( function( $id ) { - $this->assertTrue( is_numeric( $id ) ); - }, $sections ); - - // wp post objects - $sections = $course->get_sections( 'posts' ); - $this->assertEquals( 4, count( $sections ) ); - array_map( function( $post ) { - $this->assertTrue( is_a( $post, 'WP_Post' ) ); - }, $sections ); - - // section objects - $sections = $course->get_sections( 'sections' ); - $this->assertEquals( 4, count( $sections ) ); - array_map( function( $section ) { - $this->assertTrue( is_a( $section, 'LLMS_Section' ) ); - }, $sections ); - - } - - /** - * Test get_student_count() - * - * @since 4.12.0 - * - * @return void - */ - public function test_get_student_count() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $course = llms_get_post( $course_id ); - - // No value, uses default from course default property value (instead of using an empty string). - $this->assertSame( 0, $course->get_student_count() ); - - // Cached 0. - $this->assertSame( 0, $course->get_student_count() ); - - // Fake cache hit. - $course->set( 'enrolled_students', 52 ); - $this->assertSame( 52, $course->get_student_count() ); - - // Use real data. - $this->factory->student->create_and_enroll_many( 2, $course_id ); - - // Skip cache. - $this->assertSame( 2, $course->get_student_count( true ) ); - - // Cached. - $this->assertSame( 2, $course->get_student_count() ); - - } - - /** - * Test the get students function - * @return void - * @since 3.12.0 - * @version 3.12.0 - */ - public function test_get_students() { - - $this->create(); - - $students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) ); - foreach ( $students as $sid ) { - llms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' ); - } - - $this->assertEquals( 5, count( $this->obj->get_students( array( 'enrolled' ), 5 ) ) ); - $this->assertEquals( 10, count( $this->obj->get_students() ) ); - - } - - /** - * Test the has_capacity function - * @return void - * @since 3.12.0 - * @version 3.12.0 - */ - public function test_has_capacity() { - - $this->create(); - // has capacity when nothing set - $this->assertTrue( $this->obj->has_capacity() ); - - $students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) ); - foreach ( $students as $sid ) { - llms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' ); - } - - // has capacity when students enrolled and nothing set - $this->assertTrue( $this->obj->has_capacity() ); - - // enabled capacity - $this->obj->set( 'enable_capacity', 'yes' ); - $this->obj->set( 'capacity', 25 ); - - // still open - $this->assertTrue( $this->obj->has_capacity() ); - - // over capacity - $this->obj->set( 'capacity', 5 ); - $this->assertFalse( $this->obj->has_capacity() ); - - // disable capacity - $this->obj->set( 'enable_capacity', 'no' ); - $this->assertTrue( $this->obj->has_capacity() ); - - } - - /** - * Test the `has_sales_page_redirect` method. - * - * @since 3.20.0 - * @since 5.2.1 Add checks for empty URL and page ID. - */ - public function test_has_sales_page_redirect() { - - $course = new LLMS_Course( 'new', 'Course Name' ); - - $this->assertEquals( false, $course->has_sales_page_redirect() ); - - $course->set( 'sales_page_content_type', 'none' ); - $this->assertEquals( false, $course->has_sales_page_redirect() ); - - $course->set( 'sales_page_content_type', 'content' ); - $this->assertEquals( false, $course->has_sales_page_redirect() ); - - $course->set( 'sales_page_content_type', 'url' ); - $this->assertEquals( false, $course->has_sales_page_redirect() ); - - $course->set( 'sales_page_content_url', 'https://lifterlms.com' ); - $this->assertEquals( true, $course->has_sales_page_redirect() ); - - $course->set( 'sales_page_content_type', 'page' ); - $this->assertEquals( false, $course->has_sales_page_redirect() ); - - $page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) ); - $course->set( 'sales_page_content_page_id', $page_id ); - $this->assertEquals( true, $course->has_sales_page_redirect() ); - - } - - /** - * Test to_array_extra_blocks() - * - * @since 4.7.0 - * - * @return void - */ - public function test_to_array_extra_blocks() { - - // Mock reusable block. - $block_title = 'Reusable block title'; - $block_content = '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->'; - $block = $this->factory->post->create( array( - 'post_content' => $block_content, - 'post_title' => $block_title, - 'post_type' => 'wp_block', - ) ); - - // Get the HTML of the reusable block to use in our mock course content.. - $html = serialize_block( array( - 'blockName' => 'core/block', - 'innerContent' => array( '' ), - 'attrs' => array( - 'ref' => $block, - ) - ) ); - $html .= serialize_block( array( - 'blockName' => 'core/paragraph', - 'innerContent' => array( 'Lorem ipsum dolor sit.' ), - 'attrs' => array(), - ) ); - - // Mock course. - $post = $this->factory->post->create_and_get( array( - 'post_type' => 'course', - 'post_content' => $html, - ) ); - $course = llms_get_post( $post ); - - $expect = array( - $block => array( - 'title' => $block_title, - 'content' => $block_content, - ), - ); - - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $course, 'to_array_extra_blocks', array( $post->post_content ) ) ); - - } - - /** - * Test to_array_extra_images() - * - * @since 4.7.0 - * - * @return void - */ - public function test_to_array_extra_images() { - - $post = $this->factory->post->create_and_get( array( - 'post_type' => 'course', - 'post_content' => '<!-- wp:image {"id":552,"sizeSlug":"large"} --> -<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> -<!-- /wp:image --> -<!-- wp:gallery {"ids":[1,2]} --> -<figure class="wp-block-gallery columns-2 is-cropped"><ul class="blocks-gallery-grid"> -<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> -<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> -<!-- /wp:gallery --> -<img src="http://example.org/wp-content/uploads/2020/09/image1.png" alt="" class="wp-image-1" /> -<img src="http://cdn.tld/image3.png" />' - ) ); - - $expect = array( - 'http://example.org/wp-content/uploads/2020/09/image1.png', - 'http://example.org/wp-content/uploads/2020/09/image2.jpg', - ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( llms_get_post( $post ), 'to_array_extra_images', array( $post->post_content ) ) ); - - } - - /** - * Test summary get_to_array_excluded_properties() - * - * @since 5.4.1 - * - * @return void - */ - public function test_get_to_array_excluded_properties() { - - // Default behavior - $course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - $expect = array( - 'average_grade', - 'average_progress', - 'enrolled_students', - 'last_data_calc_run', - 'temp_calc_data' - ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $course, 'get_to_array_excluded_properties' ) ); - - // Disabled via filter. - add_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' ); - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $course, 'get_to_array_excluded_properties' ) ); - remove_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' ); - - } - - /** - * Test toArray to ensure no excluded properties are included. - * - * @since 5.4.1 - * - * @return void - */ - public function test_toArray_exclusion() { - - $course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - - $arr = $course->toArray(); - - $excluded = array( - 'average_grade', - 'average_progress', - 'enrolled_students', - 'last_data_calc_run', - 'temp_calc_data' - ); - - // Shouldn't contain any excluded props. - $this->assertEquals( array(), array_intersect( $excluded, array_keys( $arr ) ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php deleted file mode 100644 index 13d51a839b..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php +++ /dev/null @@ -1,633 +0,0 @@ -<?php -/** - * Tests for LifterLMS Lesson Model - * - * @group post_models - * @group lessons - * - * @since 3.14.8 - * @since 3.29.0 Unknown. - * @since 3.36.2 Added tests on lesson's availability with drip method set as 3 days after - * the course start date and empty course start date. - * Also added `$date_delta` property to be used to test dates against current time. - * @since 4.4.0 Added tests on next/previous lessons retrieval. - * @since 4.4.2 Added additional navigation testing scenarios. - * @since 4.11.0 Addeed additional tests when retrieving next/prev lesson with empty sibling sections. - */ -class LLMS_Test_LLMS_Lesson extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_Lesson'; - - /** - * Db post type of the model being tested - * - * @var string - */ - protected $post_type = 'lesson'; - - /** - * Consider dates equal for +/- 1 min - * - * @var integer - */ - private $date_delta = 60; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array. - * - * @since 3.14.8 - * @since 3.16.11 Unknown. - * @return array - */ - protected function get_properties() { - return array( - - 'order' => 'absint', - - // Drippable. - 'days_before_available' => 'absint', - 'date_available' => 'text', - 'drip_method' => 'text', - 'time_available' => 'text', - - // Parent element. - 'parent_course' => 'absint', - 'parent_section' => 'absint', - - 'audio_embed' => 'text', - 'free_lesson' => 'yesno', - 'has_prerequisite' => 'yesno', - 'prerequisite' => 'absint', - 'require_passing_grade' => 'yesno', - 'video_embed' => 'text', - - // Quizzes. - 'quiz' => 'absint', - 'quiz_enabled' => 'yesno', - - ); - } - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters. - * - * @since 3.14.8 - * @since 3.16.11 Unknown. - * - * @return array - */ - protected function get_data() { - return array( - 'audio_embed' => 'http://example.tld/audio_embed', - 'date_available' => '11/21/2018', - 'days_before_available' => '24', - 'drip_method' => 'date', - 'free_lesson' => 'no', - 'has_prerequisite' => 'yes', - 'order' => 1, - 'parent_course' => 85, - 'parent_section' => 32, - 'prerequisite' => 344, - 'quiz' => 123, - 'quiz_enabled' => 'yes', - 'require_passing_grade' => 'yes', - 'time_available' => '12:34 PM', - 'video_embed' => 'http://example.tld/video_embed', - ); - } - - - /* - /$$ /$$ - | $$ | $$ - /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$ - |_ $$_/ /$$__ $$ /$$_____/|_ $$_/ /$$_____/ - | $$ | $$$$$$$$| $$$$$$ | $$ | $$$$$$ - | $$ /$$| $$_____/ \____ $$ | $$ /$$\____ $$ - | $$$$/| $$$$$$$ /$$$$$$$/ | $$$$//$$$$$$$/ - \___/ \_______/|_______/ \___/ |_______/ - */ - - /** - * Test get available date. - * - * @since Unknown. - * @since 3.36.2 Added tests on lesson's availability with drip method set as 3 days after - * the course start date and empty course start date. - * @since 5.3.3 Use `assertEqualsWithDelta()`. - * - * @return void - */ - public function test_get_available_date() { - - $format = 'Y-m-d'; - - $course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0]; - $course = llms_get_post( $course_id ); - $lesson = $course->get_lessons()[0]; - $lesson_id = $lesson->get( 'id' ); - $student = $this->get_mock_student(); - wp_set_current_user( $student->get_id() ); - $student->enroll( $course_id ); - - // No drip settings, lesson is currently available. - $this->assertEquals( current_time( $format ), $lesson->get_available_date( $format ) ); - - $lesson->set( 'drip_method', 'date' ); - $lesson->set( 'date_available', '12/12/2012' ); - $lesson->set( 'time_available', '12:12 AM' ); - $this->assertEquals( date( $format, strtotime( '12/12/2012' ) ), $lesson->get_available_date( $format ) ); - $this->assertEquals( date( 'U', strtotime( '12/12/2012 12:12 AM' ) ), $lesson->get_available_date( 'U' ) ); - - $lesson->set( 'drip_method', 'enrollment' ); - $lesson->set( 'days_before_available', '3' ); - $this->assertEquals( $student->get_enrollment_date( $course_id, 'enrolled', 'U' ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) ); - - $lesson->set( 'drip_method', 'start' ); - $start = current_time( 'm/d/Y' ); - $course->set( 'start_date', $start ); - $this->assertEquals( strtotime( $start ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) ); - - $prereq_id = $lesson_id; - $student->mark_complete( $lesson_id, 'lesson' ); - - $lesson = $course->get_lessons()[1]; - - $lesson->set( 'has_prerequisite', 'yes' ); - $lesson->set( 'prerequisite', $lesson_id ); - - $lesson->set( 'drip_method', 'prerequisite' ); - $lesson->set( 'days_before_available', '3' ); - $this->assertEquals( $student->get_completion_date( $prereq_id, 'U' ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) ); - - // Check lesson immediately available if set to be available after 3 days ofter a course start date which is empty. - $lesson->set( 'drip_method', 'start' ); - $lesson->set( 'days_before_available', '3' ); - $course->set( 'start_date', '' ); - $this->assertEqualsWithDelta( current_time( 'timestamp' ), $lesson->get_available_date( 'U' ), $this->date_delta ); - - } - - /** - * Test get course - * - * @since unknown - * - * @return void - */ - public function test_get_course() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson = llms_get_post( $course->get_lessons( 'ids' )[0] ); - - // Returns a course when everything's okay. - $this->assertTrue( is_a( $lesson->get_course(), 'LLMS_Course' ) ); - - // Course trashed / doesn't exist, returns null. - wp_delete_post( $course->get( 'id' ), true ); - $this->assertNull( $lesson->get_course() ); - - } - - /** - * Test Audio and Video Embeds - * - * @since 3.14.8 - * @since 4.10.0 Fix faulty tests, use assertSame in favor of assertEquals. - * - * @return void - */ - public function test_get_embeds() { - - $lesson = new LLMS_Lesson( 'new', 'Lesson With Embeds' ); - - $audio_url = 'https://open.spotify.com/track/1rNUOtuCWv1qswqsMFvzvz'; - $video_url = 'https://www.youtube.com/watch?v=MhQlNwxn5oo'; - - // Empty string when none set. - $this->assertEmpty( $lesson->get_audio() ); - $this->assertEmpty( $lesson->get_video() ); - - $lesson->set( 'audio_embed', $audio_url ); - $lesson->set( 'video_embed', $video_url ); - - $audio_embed = $lesson->get_audio(); - $video_embed = $lesson->get_video(); - - // Should be an iframe for valid embeds. - $this->assertSame( 0, strpos( $audio_embed, '<iframe' ) ); - $this->assertSame( 0, strpos( $video_embed, '<iframe' ) ); - - // Fallbacks should be a link to the URL. - $not_embeddable_url = 'http://lifterlms.com/not/embeddable'; - - $lesson->set( 'audio_embed', $not_embeddable_url ); - $lesson->set( 'video_embed', $not_embeddable_url ); - $audio_embed = $lesson->get_audio(); - $video_embed = $lesson->get_video(); - - $this->assertSame( 0, strpos( $audio_embed, '<a' ) ); - $this->assertSame( 0, strpos( $video_embed, '<a' ) ); - - $this->assertStringContains( sprintf( 'href="%s"', $not_embeddable_url ), $audio_embed ); - $this->assertStringContains( sprintf( 'href="%s"', $not_embeddable_url ), $video_embed ); - - } - - /** - * Test getting parent section - * - * @since unknown - * - * @return void - */ - public function test_get_section() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $lesson = llms_get_post( $course->get_lessons( 'ids' )[0] ); - - // Returns a course when everything's okay. - $this->assertTrue( is_a( $lesson->get_section(), 'LLMS_Section' ) ); - - // Section trashed / doesn't exist, returns null. - wp_delete_post( $lesson->get( 'parent_section' ), true ); - $this->assertNull( $lesson->get_section() ); - - } - - /** - * Test has_modified_slug function - * - * @since 3.14.8 - * - * @return void - */ - public function test_has_modified_slug() { - - $lesson = new LLMS_Lesson( 'new', 'New Lesson' ); - - // Default unmodified slug. - $this->assertFalse( $lesson->has_modified_slug() ); - - // Default unmodified slug with a unique int at the end. - $lesson->set( 'name', 'new-lesson-123' ); - - $this->assertFalse( $lesson->has_modified_slug() ); - - // Renamed slug. - $lesson->set( 'name', 'modified-slug' ); - - $this->assertTrue( $lesson->has_modified_slug() ); - - } - - /** - * Test the has_quiz() method - * - * @since 3.29.0 - * - * @return void - */ - public function test_has_quiz() { - - $lesson = new LLMS_Lesson( 'new', 'New Lesson' ); - - $this->assertFalse( $lesson->has_quiz() ); - $lesson->set( 'quiz', 123 ); - $this->assertTrue( $lesson->has_quiz() ); - - } - - /** - * Test the is_available() method - * - * @since unknown - * - * @return void - */ - public function test_is_available() { - - $course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0]; - $course = llms_get_post( $course_id ); - $lesson = $course->get_lessons()[0]; - $lesson_id = $lesson->get( 'id' ); - $student = $this->get_mock_student(); - wp_set_current_user( $student->get_id() ); - $student->enroll( $course_id ); - - // No drip settings, lesson is currently available. - $this->assertTrue( $lesson->is_available() ); - - // Date in past so the lesson is available. - $lesson = llms_get_post( $lesson_id ); - $lesson->set( 'drip_method', 'date' ); - $lesson->set( 'date_available', '12/12/2012' ); - $lesson->set( 'time_available', '12:12 AM' ); - $this->assertTrue( $lesson->is_available() ); - - // Date in future so lesson not available. - $lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) ); - $this->assertFalse( $lesson->is_available() ); - - // Available 3 days after enrollment. - $lesson->set( 'drip_method', 'enrollment' ); - $lesson->set( 'days_before_available', '3' ); - $this->assertFalse( $lesson->is_available() ); - - // Now available. - llms_mock_current_time( '+4 days' ); - $this->assertTrue( $lesson->is_available() ); - - llms_reset_current_time(); - $lesson->set( 'drip_method', 'start' ); - $course->set( 'start_date', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) ); - - // Not available until 3 days after course start date. - $this->assertFalse( $lesson->is_available() ); - - // Now available. - llms_mock_current_time( '+4 days' ); - $this->assertTrue( $lesson->is_available() ); - llms_reset_current_time(); - - $prereq_id = $lesson_id; - $student->mark_complete( $lesson_id, 'lesson' ); - - // Second lesson not available until 3 days after lesson 1 is complete. - $lesson = $course->get_lessons()[1]; - - $lesson->set( 'has_prerequisite', 'yes' ); - $lesson->set( 'prerequisite', $lesson_id ); - - $lesson->set( 'drip_method', 'prerequisite' ); - $lesson->set( 'days_before_available', '3' ); - - $this->assertFalse( $lesson->is_available() ); - - llms_mock_current_time( '+4 days' ); - $this->assertTrue( $lesson->is_available() ); - - } - - /** - * Test the is_orphan() method - * - * @since 3.14.8 - * - * @return void - */ - public function test_is_orphan() { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] ); - $section = llms_get_post( $course->get_sections( 'ids' )[0] ); - $lesson = llms_get_post( $course->get_lessons( 'ids' )[0] ); - - // Not an orphan. - $this->assertFalse( $lesson->is_orphan() ); - - $test_statuses = get_post_stati( array( '_builtin' => true ) ); - foreach ( array_keys( $test_statuses ) as $status ) { - - $assert = in_array( $status, array( 'publish', 'future', 'draft', 'pending', 'private', 'auto-draft' ), true ) ? 'assertFalse' : 'assertTrue'; - - // Check parent course. - wp_update_post( - array( - 'ID' => $course->get( 'id' ), - 'post_status' => $status, - ) - ); - $this->$assert( $lesson->is_orphan() ); - wp_update_post( - array( - 'ID' => $course->get( 'id' ), - 'post_status' => 'publish', - ) - ); - - // Check parent section. - wp_update_post( - array( - 'ID' => $section->get( 'id' ), - 'post_status' => $status, - ) - ); - $this->$assert( $lesson->is_orphan() ); - wp_update_post( - array( - 'ID' => $section->get( 'id' ), - 'post_status' => 'publish', - ) - ); - - } - - // Parent course doesn't exist. - $lesson->set( 'parent_course', 123456789 ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_course', $course->get( 'id' ) ); - - // Parent section doesn't exist. - $lesson->set( 'parent_section', 123456789 ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_section', $section->get( 'id' ) ); - - // Parent course isn't set. - $lesson->set( 'parent_course', '' ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_course', $course->get( 'id' ) ); - - // Parent section isn't set. - $lesson->set( 'parent_section', '' ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_section', $section->get( 'id' ) ); - - // Metakey for parent course doesn't exist. - delete_post_meta( $lesson->get( 'id' ), '_llms_parent_course' ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_course', $course->get( 'id' ) ); - - // Metakey for parent section doesn't exist. - delete_post_meta( $lesson->get( 'id' ), '_llms_parent_section' ); - $this->assertTrue( $lesson->is_orphan() ); - $lesson->set( 'parent_section', $section->get( 'id' ) ); - - // Not an orphan. - $this->assertFalse( $lesson->is_orphan() ); - - } - - /** - * Test next lesson - * - * @since 4.4.0 - */ - public function test_get_next_lesson() { - - // Generate a course with 2 sections and 3 lessons for each of them. - $course_id = $this->generate_mock_courses( 1, 2, 3, 0 )[0]; - $section_ids = llms_get_post( $course_id )->get_sections( 'ids' ); - $section_one = llms_get_post( $section_ids[0] ); - $section_two = llms_get_post( $section_ids[1] ); - - $sec_lessons = array( - $section_one->get_lessons( 'ids' ), - $section_two->get_lessons( 'ids' ), - ); - - // Test next lesson of s1 l2 is s1 l3. - $this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][1] )->get_next_lesson() ); - - // Test next lesson of s1 l3 is s2 l1. - $this->assertEquals( $sec_lessons[1][0], llms_get_post( $sec_lessons[0][2] )->get_next_lesson() ); - - // Swap s1 l2 and s1 l3 orders. - llms_get_post( $sec_lessons[0][1] )->set( 'order', 3 ); - llms_get_post( $sec_lessons[0][2] )->set( 'order', 2 ); - - // Test next lesson of s1 l1 is the original s1 l3. - $this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][0] )->get_next_lesson() ); - // "Persist" the new order in our sec_lessons array. - list( $sec_lessons[0][2], $sec_lessons[0][1] ) = array( $sec_lessons[0][1], $sec_lessons[0][2] ); - - // Test s2 l3 has no next lesson. - $this->assertFalse( llms_get_post( $sec_lessons[1][2] )->get_next_lesson() ); - - // Unpublish s1 l2, test next lesson of s1 l1 is s1 l3. - llms_get_post( $sec_lessons[0][1] )->set( 'status', 'draft' ); - $this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][0] )->get_next_lesson() ); - - // Unpublish s2 l3, test next lesson of s2 l2 is false. - llms_get_post( $sec_lessons[1][2] )->set( 'status', 'draft' ); - $this->assertFalse( llms_get_post( $sec_lessons[1][1] )->get_next_lesson() ); - } - - - /** - * Test previous lesson - * - * @since 4.4.0 - */ - public function test_get_previous_lesson() { - - // Generate a course with 2 sections and 3 lessons for each of them. - $course_id = $this->generate_mock_courses( 1, 2, 3, 0 )[0]; - $section_ids = llms_get_post( $course_id )->get_sections( 'ids' ); - $section_one = llms_get_post( $section_ids[0] ); - $section_two = llms_get_post( $section_ids[1] ); - - $sec_lessons = array( - $section_one->get_lessons( 'ids' ), - $section_two->get_lessons( 'ids' ), - ); - - // Test previous lesson of s1 l3 is s1 l2. - $this->assertEquals( $sec_lessons[0][1], llms_get_post( $sec_lessons[0][2] )->get_previous_lesson() ); - - // Test previous lesson of s2 l1 is s1 l3. - $this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[1][0] )->get_previous_lesson() ); - - // Swap s1 l1 and s1 l2 orders. - llms_get_post( $sec_lessons[0][0] )->set( 'order', 2 ); - llms_get_post( $sec_lessons[0][1] )->set( 'order', 1 ); - - // Test previous lesson of s1 l3 is the original s1 l1. - $this->assertEquals( $sec_lessons[0][0], llms_get_post( $sec_lessons[0][2] )->get_previous_lesson() ); - // "Persist" the new order in our sec_lessons array. - list( $sec_lessons[0][0], $sec_lessons[0][1] ) = array( $sec_lessons[0][1], $sec_lessons[0][0] ); - - // Test s1 l1 has no previous lesson. - $this->assertFalse( llms_get_post( $sec_lessons[0][0] )->get_previous_lesson() ); - - // Unpublish s2 l2, test previous lesson of s2 l3 is s2 l1. - llms_get_post( $sec_lessons[1][1] )->set( 'status', 'draft' ); - $this->assertEquals( $sec_lessons[1][0], llms_get_post( $sec_lessons[1][2] )->get_previous_lesson() ); - - // Unpublish s2 l1, test previous lesson of s2 l2 is s1 l3. - llms_get_post( $sec_lessons[1][0] )->set( 'status', 'draft' ); - $this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[1][1] )->get_previous_lesson() ); - - // Unpublish s1 l1, test previous lesson of s1 l2 is false. - llms_get_post( $sec_lessons[0][0] )->set( 'status', 'draft' ); - $this->assertFalse( llms_get_post( $sec_lessons[0][1] )->get_previous_lesson() ); - - } - - /** - * Test navigation with sections that have more than 10 lessons - * - * This scenario exposes an issue that causes string comparisons to fail, the lesson order will be returned - * incorrectly. - * - * @since 4.4.2 - * - * @link https://github.com/gocodebox/lifterlms/issues/1316 - * - * @return void - */ - public function test_navigation_large_sections() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 2, 'lessons' => 10, 'quizzes' => 0 ) ); - $lessons = $course->get_lessons(); - - $i = 0; - while ( $i < count( $lessons ) ) { - - $lesson = $lessons[ $i ]; - - $next = 19 === $i ? false : $lessons[ $i + 1 ]->get( 'id' ); - $prev = 0 === $i ? false : $lessons[ $i - 1 ]->get( 'id' ); - - $this->assertEquals( $next, $lesson->get_next_lesson(), $i ); - $this->assertEquals( $prev, $lesson->get_previous_lesson(), $i ); - - ++$i; - - } - - } - - /** - * Test next/prev lesson with empty sibling sections - * - * @since 4.11.0 - * - * @return void - */ - public function test_navigation_empty_sibling_section() { - - $course = $this->factory->course->create_and_get( - array( - 'sections' => 2, - 'lessons' => 1, - 'quizzes' => 0 - ) - ); - - $lessons = $course->get_lessons(); - - // Detach the second lesson from the second section. - $second_section = $lessons[1]->get_parent_section(); - $lessons[1]->set_parent_section(''); - // Check the next lesson of the first one is false. - $this->assertEquals( false, $lessons[0]->get_next_lesson() ); - - // Re-attach the second lesson to the second section. - $lessons[1]->set_parent_section( $second_section ); - - // Detach the first lesson from the first section. - $lessons[0]->set_parent_section(''); - // Check the previous lesson of the second one is false. - $this->assertEquals( false, $lessons[1]->get_previous_lesson() ); - - } -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-membership.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-membership.php deleted file mode 100644 index 15a71155fd..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-membership.php +++ /dev/null @@ -1,416 +0,0 @@ -<?php -/** - * Tests for LifterLMS Membership Model. - * - * @group LLMS_Membership - * @group LLMS_Post_Model - * - * @since 3.20.0 - * @since 3.36.3 Remove redundant test method `test_get_sections()`, - * @see tests/unit-tests/models/class-llms-test-model-llms-course.php. - * @since 5.2.1 Add checks for empty URL and page ID in `test_has_sales_page_redirect()`. - */ -class LLMS_Test_LLMS_Membership extends LLMS_PostModelUnitTestCase { - - /** - * class name for the model being tested by the class. - * @var string - */ - protected $class_name = 'LLMS_Membership'; - - /** - * db post type of the model being tested. - * @var string - */ - protected $post_type = 'llms_membership'; - - /** - * Get properties, used by test_getters_setters. - * This should match, exactly, the object's $properties array. - * - * @since 3.20.0 - * - * @return array - */ - protected function get_properties() { - return array( - 'auto_enroll' => 'array', - 'redirect_page_id' => 'absint', - 'restriction_add_notice' => 'yesno', - 'restriction_notice' => 'html', - 'restriction_redirect_type' => 'text', - 'redirect_custom_url' => 'text', - 'sales_page_content_page_id' => 'absint', - 'sales_page_content_type' => 'string', - 'sales_page_content_url' => 'string', - ); - } - - /** - * Get data to fill a create post with. - * This is used by test_getters_setters. - * - * @since 3.20.0 - * - * @return array - */ - protected function get_data() { - return array( - 'auto_enroll' => array(), - 'redirect_page_id' => '1', - 'restriction_add_notice' => 'yes', - 'restriction_notice' => '<p>test</p>', - 'restriction_redirect_type' => 'none', - 'redirect_custom_url' => 'https://lifterlms.com', - 'sales_page_content_page_id' => 1, - 'sales_page_content_type' => 'none', - 'sales_page_content_url' => 'https://lifterlms.com', - ); - } - - /** - * Test CRUD functions for auto enroll courses. - * - * Tests the following three methods: - * - * + add_auto_enroll_courses() - * + get_auto_enroll_courses() - * + remoe_auto_enroll_course() - * - * @since 4.15.0 - * - * @return void - */ - public function test_crud_auto_enroll() { - - $membership = $this->factory->membership->create_and_get(); - - // No posts. - $this->assertSame( array(), $membership->get_auto_enroll_courses() ); - - // Add a single course (not an array). - $course = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->assertTrue( $membership->add_auto_enroll_courses( $course ) ); - $this->assertSame( array( $course ), $membership->get_auto_enroll_courses() ); - - // Add multiple courses (as an array). - $courses = $this->factory->post->create_many( 2, array( 'post_type' => 'course' ) ); - $this->assertTrue( $membership->add_auto_enroll_courses( $courses ) ); - $this->assertEqualSets( array_merge( array( $course ), $courses ), $membership->get_auto_enroll_courses() ); - - // Remove a course. - $this->assertTrue( $membership->remove_auto_enroll_course( $course ) ); - $this->assertEqualSets( $courses, $membership->get_auto_enroll_courses() ); - - // Add a course that already exists (should remove duplicates). - $this->assertTrue( $membership->add_auto_enroll_courses( $courses[1] ) ); - $this->assertEqualSets( $courses, $membership->get_auto_enroll_courses() ); - - // Add & replace. - $this->assertTrue( $membership->add_auto_enroll_courses( $course, true ) ); - $this->assertEquals( array( $course ), $membership->get_auto_enroll_courses() ); - - } - - /** - * Ensure only published courses - * - * @since 4.15.0 - * - * @link https://github.com/gocodebox/lifterlms-groups/issues/135 - * - * @return void - */ - public function test_get_auto_enroll_courses_published_only() { - - $membership = $this->factory->membership->create_and_get(); - $draft = $this->factory->post->create( array( 'post_type' => 'course', 'post_status' => 'draft' ) ); - $private = $this->factory->post->create( array( 'post_type' => 'course', 'post_status' => 'private' ) ); - $published = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $this->assertTrue( $membership->add_auto_enroll_courses( array( $draft, $private, $published ) ) ); - $this->assertEqualSets( array( $published ), $membership->get_auto_enroll_courses() ); - - } - - /** - * Test get_associated_posts() when none exist for the membership. - * - * @since 3.38.1 - * - * @return void - */ - public function test_get_associated_posts_none_found() { - - $membership = $this->factory->membership->create_and_get(); - - $this->assertEquals( array(), $membership->get_associated_posts() ); - - $this->assertEquals( array(), $membership->get_associated_posts( 'course' ) ); - $this->assertEquals( array(), $membership->get_associated_posts( 'page' ) ); - $this->assertEquals( array(), $membership->get_associated_posts( 'post' ) ); - $this->assertEquals( array(), $membership->get_associated_posts( 'fake' ) ); - - } - - /** - * Test get_associated_posts() when associations do exist. - * - * @since 3.38.1 - * @since 4.15.0 Test equal sets instead of strict equals because we don't really care about the returned order. - * Added tests to check when querying for a single post type. - * - * @return void - */ - public function test_get_associated_posts_has_associations() { - - $membership = $this->factory->membership->create_and_get(); - - // Add a post. - $post = $this->factory->post->create(); - update_post_meta( $post, '_llms_is_restricted', 'yes' ); - update_post_meta( $post, '_llms_restricted_levels', array( $membership->get( 'id' ), 1, 1008, '183' ) ); - - // Add pages. - $page1 = $this->factory->post->create( array( 'post_type' => 'page' ) ); - update_post_meta( $page1, '_llms_is_restricted', 'yes' ); - update_post_meta( $page1, '_llms_restricted_levels', array( (string) $membership->get( 'id' ) . '00', $membership->get( 'id' ), 1234, 2 ) ); - - $page2 = $this->factory->post->create( array( 'post_type' => 'page' ) ); - update_post_meta( $page2, '_llms_is_restricted', 'yes' ); - update_post_meta( $page2, '_llms_restricted_levels', array( $membership->get( 'id' ) ) ); - - // Add a course with a plan. - $plan = $this->get_mock_plan(); - $plan->set( 'availability', 'members' ); - $plan->set( 'availability_restrictions', array( 1, $membership->get( 'id' ) ) ); - - // Add an autoenrollment course. - $course = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $membership->set( 'auto_enroll', array( $course, $plan->get( 'product_id' ) ) ); - - // Get all associations. - $res = $membership->get_associated_posts(); - - $this->assertEquals( array( $post ), $res['post'] ); - $this->assertEqualSets( array( $page1, $page2 ), $res['page'] ); - $this->assertEqualSets( array( $plan->get( 'product_id' ), $course ), $res['course'] ); - - // Get only course associations. - $res = $membership->get_associated_posts( 'course' ); - $this->assertEqualSets( array( $plan->get( 'product_id' ), $course ), $res ); - - // Get posts. - $res = $membership->get_associated_posts( 'post' ); - $this->assertEquals( array( $post ), $res ); - - // Get pages. - $res = $membership->get_associated_posts( 'page' ); - $this->assertEqualSets( array( $page1, $page2 ), $res ); - - // Fake post type. - $res = $membership->get_associated_posts( 'fake' ); - $this->assertEqualSets( array(), $res ); - - - } - - /** - * Test LLMS_Membership->get_categories() method. - * - * @since 3.36.3 - * - * @return void - */ - public function test_get_categories() { - // create new membership - $membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ); - $membership = new LLMS_Membership( $membership_id ); - - // create new categories - $taxonomy = 'membership_cat'; - $created_term_ids = array(); - for ( $i = 1; $i <= 3; $i ++ ) { - $new_term_ids = wp_create_term( "mock-membership-category-$i", $taxonomy ); - $this->assertNotWPError( $new_term_ids ); - $created_term_ids[ $i ] = $new_term_ids['term_id']; - } - - // set categories in membership - $term_taxonomy_ids = wp_set_post_terms( $membership_id, $created_term_ids, $taxonomy ); - $this->assertNotWPError( $term_taxonomy_ids ); - $this->assertNotFalse( $term_taxonomy_ids ); - - // get categories from membership - $membership_terms = $membership->get_categories(); - $membership_term_ids = array(); - /** @var WP_Term $membership_term */ - foreach ( $membership_terms as $membership_term ) { - $membership_term_ids[] = $membership_term->term_id; - } - - // compare array values while ignoring keys and order - $this->assertEqualSets( $created_term_ids, $membership_term_ids ); - } - - /** - * Test get_product() - * - * @since 4.15.0 - * - * @return void - */ - public function test_get_product() { - - $membership = $this->factory->membership->create_and_get(); - - $this->assertInstanceOf( 'LLMS_Product', $membership->get_product() ); - - } - - /** - * Test LLMS_Membership->get_tags() method. - * - * @since 3.36.3 - * @return void - */ - public function test_get_tags() { - // create new membership - $membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ); - $membership = new LLMS_Membership( $membership_id ); - - // create new tags - $taxonomy = 'membership_tag'; - $created_term_ids = array(); - for ( $i = 1; $i <= 3; $i ++ ) { - $new_term_ids = wp_create_term( "mock-membership-tag-$i", $taxonomy ); - $this->assertNotWPError( $new_term_ids ); - $created_term_ids[ $i ] = $new_term_ids['term_id']; - } - - // set tags in membership - $term_taxonomy_ids = wp_set_post_terms( $membership_id, $created_term_ids, $taxonomy ); - $this->assertNotWPError( $term_taxonomy_ids ); - $this->assertNotFalse( $term_taxonomy_ids ); - - // get tags from membership - $membership_terms = $membership->get_tags(); - $membership_term_ids = array(); - /** @var WP_Term $membership_term */ - foreach ( $membership_terms as $membership_term ) { - $membership_term_ids[] = $membership_term->term_id; - } - - // compare array values while ignoring keys and order - $this->assertEqualSets( $created_term_ids, $membership_term_ids ); - } - - /** - * Test get_sales_page_url method. - * - * @since 3.20.0 - * - * @return void - */ - public function test_get_sales_page_url() { - - $course = new LLMS_Membership( 'new', 'Membership Title' ); - - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'none' ); - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'content' ); - $this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'url' ); - $course->set( 'sales_page_content_url', 'https://lifterlms.com' ); - $this->assertEquals( 'https://lifterlms.com', $course->get_sales_page_url() ); - - $course->set( 'sales_page_content_type', 'page' ); - $page = $this->factory->post->create(); - $course->set( 'sales_page_content_page_id', $page ); - $this->assertEquals( get_permalink( $page ), $course->get_sales_page_url() ); - - } - - /** - * Test the get students function. - * - * @since 3.12.0 - * - * @return void - */ - public function test_get_students() { - - $this->create(); - - $students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) ); - foreach ( $students as $sid ) { - llms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' ); - } - - $this->assertEquals( 5, count( $this->obj->get_students( array( 'enrolled' ), 5 ) ) ); - $this->assertEquals( 10, count( $this->obj->get_students() ) ); - - } - - /** - * Test the `has_sales_page_redirect` method. - * - * @since 3.20.0 - * @since 5.2.1 Add checks for empty URL and page ID. - */ - public function test_has_sales_page_redirect() { - - $membership = new LLMS_Membership( 'new', 'Membership Title' ); - - $this->assertEquals( false, $membership->has_sales_page_redirect() ); - - $membership->set( 'sales_page_content_type', 'none' ); - $this->assertEquals( false, $membership->has_sales_page_redirect() ); - - $membership->set( 'sales_page_content_type', 'content' ); - $this->assertEquals( false, $membership->has_sales_page_redirect() ); - - $membership->set( 'sales_page_content_type', 'url' ); - $this->assertEquals( false, $membership->has_sales_page_redirect() ); - - $membership->set( 'sales_page_content_url', 'https://lifterlms.com' ); - $this->assertEquals( true, $membership->has_sales_page_redirect() ); - - $membership->set( 'sales_page_content_type', 'page' ); - $this->assertEquals( false, $membership->has_sales_page_redirect() ); - - $page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) ); - $membership->set( 'sales_page_content_page_id', $page_id ); - $this->assertEquals( true, $membership->has_sales_page_redirect() ); - - } - - /** - * Test query_associated_courses() to ensure only plan associations from published courses are returned. - * - * @since 4.15.0 - * - * @link https://github.com/gocodebox/lifterlms-groups/issues/135 - * - * @return void - */ - public function test_query_associated_courses_published_only() { - - $membership = $this->factory->membership->create_and_get(); - - $plan = $this->get_mock_plan(); - $plan->set( 'availability', 'members' ); - $plan->set( 'availability_restrictions', array( 1, $membership->get( 'id' ) ) ); - - $course = llms_get_post( $plan->get( 'product_id' ) ); - $course->set( 'status', 'draft' ); - - $this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $membership, 'query_associated_courses' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-order.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-order.php deleted file mode 100644 index 68b27fa482..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-order.php +++ /dev/null @@ -1,1447 +0,0 @@ -<?php -/** - * Tests for LifterLMS Course Model - * - * @package LifterLMS/Tests - * - * @group orders - * @group LLMS_Order - * @group LLMS_Post_Model - * - * @since 3.10.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 3.37.2 Add additional recurring payment tests. - * @since 3.37.6 Adjusted date delta for recurring payment next date assertions. - * Added default test override for test_edit_date() test to prevent output - * of skipped test that doesn't apply to the order model. - * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`, `unschedule_expiration()`, and `unschedule_recurring_payment()`. - * @since 4.7.0 Update tests to handle new meta property `plan_ended`. - */ -class LLMS_Test_LLMS_Order extends LLMS_PostModelUnitTestCase { - - /** - * Consider dates equal for +/- 2 mins - * @var integer - */ - private $date_delta = 120; - - /** - * Setup the test case. - * - * @since Unknown - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->create(); - } - - /** - * Add support for a payment gateway feature. - * - * @since Unknown - * - * @param string $feature Feature name - * @return void - */ - private function mock_gateway_support( $feature ) { - - global $llms_mock_gateway_feature; - $llms_mock_gateway_feature = $feature; - - add_filter( 'llms_get_gateway_supported_features', function( $features ) { - global $llms_mock_gateway_feature; - $features[ $llms_mock_gateway_feature ] = true; - return $features; - } ); - - } - - private function get_plan( $price = 25.99, $frequency = 1, $expiration = 'lifetime', $on_sale = false, $trial = false ) { - - return $this->get_mock_plan( $price, $frequency, $expiration, $on_sale, $trial ); - - } - - private function get_order( $plan = null, $coupon = false ) { - - return $this->get_mock_order( $plan, $coupon ); - - } - - /** - * Class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_Order'; - - /** - * Db post type of the model being tested - * - * @var string - */ - protected $post_type = 'llms_order'; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array - * - * @since 3.10.0 - * - * @return string[] - */ - protected function get_properties() { - return array( - - 'coupon_amount' => 'float', - 'coupon_amout_trial' => 'float', - 'coupon_value' => 'float', - 'coupon_value_trial' => 'float', - 'original_total' => 'float', - 'sale_price' => 'float', - 'sale_value' => 'float', - 'total' => 'float', - 'trial_original_total' => 'float', - 'trial_total' => 'float', - - 'access_length' => 'absint', - 'billing_frequency' => 'absint', - 'billing_length' => 'absint', - 'coupon_id' => 'absint', - 'plan_id' => 'absint', - 'product_id' => 'absint', - 'trial_length' => 'absint', - 'user_id' => 'absint', - - 'access_expiration' => 'text', - 'access_expires' => 'text', - 'access_period' => 'text', - 'billing_address_1' => 'text', - 'billing_address_2' => 'text', - 'billing_city' => 'text', - 'billing_country' => 'text', - 'billing_email' => 'text', - 'billing_first_name' => 'text', - 'billing_last_name' => 'text', - 'billing_state' => 'text', - 'billing_zip' => 'text', - 'billing_period' => 'text', - 'coupon_code' => 'text', - 'coupon_type' => 'text', - 'coupon_used' => 'text', - 'currency' => 'text', - 'on_sale' => 'text', - 'order_key' => 'text', - 'order_type' => 'text', - 'payment_gateway' => 'text', - 'plan_sku' => 'text', - 'plan_title' => 'text', - 'product_sku' => 'text', - 'product_type' => 'text', - 'title' => 'text', - 'gateway_api_mode' => 'text', - 'gateway_customer_id' => 'text', - 'trial_offer' => 'text', - 'trial_period' => 'text', - 'user_ip_address' => 'text', - - ); - } - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters. - * - * @since 3.10.0 - * - * @return array - */ - protected function get_data() { - return array( - - 'coupon_amount' => 1.00, - 'coupon_amout_trial' => 0.50, - 'coupon_value' => 1.00, - 'coupon_value_trial' => 1234234.00, - 'original_total' => 25.93, - 'sale_price' => 25.23, - 'sale_value' => 2325.00, - 'total' => 12325.00, - 'trial_original_total' => 25.00, - 'trial_total' => 123.43, - - 'access_length' => 1, - 'billing_frequency' => 1, - 'billing_length' => 1, - 'coupon_id' => 1, - 'plan_id' => 1, - 'product_id' => 1, - 'trial_length' => 1, - 'user_id' => 1, - - 'access_expiration' => 'text', - 'access_expires' => 'text', - 'access_period' => 'text', - 'billing_address_1' => 'text', - 'billing_address_2' => 'text', - 'billing_city' => 'text', - 'billing_country' => 'text', - 'billing_email' => 'text', - 'billing_first_name' => 'text', - 'billing_last_name' => 'text', - 'billing_state' => 'text', - 'billing_zip' => 'text', - 'billing_period' => 'text', - 'coupon_code' => 'text', - 'coupon_type' => 'text', - 'coupon_used' => 'text', - 'currency' => 'text', - 'on_sale' => 'text', - 'order_key' => 'text', - 'order_type' => 'single', - 'payment_gateway' => 'text', - 'plan_sku' => 'text', - 'plan_title' => 'text', - 'product_sku' => 'text', - 'product_type' => 'text', - 'title' => 'test title', - 'gateway_api_mode' => 'text', - 'gateway_customer_id' => 'text', - 'trial_offer' => 'text', - 'trial_period' => 'text', - 'user_ip_address' => 'text', - - ); - } - - /** - * Test the add_note() method - * - * @since 3.10.0 - * - * @return void - */ - public function test_add_note() { - - // Don't create empty notes. - $this->assertNull( $this->obj->add_note( '' ) ); - - $note_text = 'This is an order note'; - $id = $this->obj->add_note( $note_text ); - - // Should return the comment id. - $this->assertTrue( is_numeric( $id ) ); - - $note = get_comment( $id ); - - // Should be a comment. - $this->assertTrue( is_a( $note, 'WP_Comment' ) ); - - // Comment content should be our original note. - $this->assertEquals( $note->comment_content, $note_text ); - // Author should be the system (LifterLMS). - $this->assertEquals( $note->comment_author, 'LifterLMS' ); - - // Create a new note by a user. - $id = $this->obj->add_note( $note_text, true ); - $note = get_comment( $id ); - $this->assertEquals( get_current_user_id(), $note->user_id ); - - // 1 for original creation note, 2 for our test notes. - $this->assertEquals( 3, did_action( 'llms_new_order_note_added' ) ); - - } - - /** - * Test the can_be_retried() method. - * - * @since Unknown. - * @since 5.2.1 Add assertions for checking against single payment orders and - * when the recurring retry feature option is disabled. - * - * @return void - */ - public function test_can_be_retried() { - - $order = $this->get_order(); - - // Pending order can't be retried. - $this->assertFalse( $order->can_be_retried() ); - - // Active can be retried. - $order->set_status( 'llms-active' ); - - // Gateway doesn't support retries. - $this->assertFalse( $order->can_be_retried() ); - - // Allow the gateway to support retries. - $this->mock_gateway_support( 'recurring_retry' ); - - // Can be retried now. - $this->assertTrue( $order->can_be_retried() ); - - // On hold can be retried. - $order->set_status( 'llms-on-hold' ); - $this->assertTrue( $order->can_be_retried() ); - - // Retry disabled. - update_option( 'lifterlms_recurring_payment_retry', 'no' ); - $this->assertFalse( $order->can_be_retried() ); - update_option( 'lifterlms_recurring_payment_retry', 'yes' ); - - // Single payment cannot be retried. - $order->set( 'order_type', 'single' ); - $this->assertFalse( $order->can_be_retried() ); - - } - - /** - * Test the can_resubscribe() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_can_resubscribe() { - - $statuses = array( - 'llms-completed' => false, - 'llms-active' => false, - 'llms-expired' => false, - 'llms-on-hold' => true, - 'llms-pending-cancel' => true, - 'llms-pending' => true, - 'llms-cancelled' => false, - 'llms-refunded' => false, - 'llms-failed' => false, - ); - - foreach ( $statuses as $status => $expect ) { - - $this->obj->set( 'order_type', 'single' ); - $this->obj->set_status( $status ); - $this->assertEquals( false, $this->obj->can_resubscribe() ); - - $this->obj->set( 'order_type', 'recurring' ); - $this->obj->set_status( $status ); - $this->assertEquals( $expect, $this->obj->can_resubscribe() ); - } - - } - - /** - * Test creation of the model. - * - * @since 5.9.0 - * - * @return void - */ - public function test_create_model() { - - $date = '2021-04-22 14:34:00'; - llms_mock_current_time( $date ); - - $this->create( '' ); - - $id = $this->obj->get( 'id' ); - - $test = llms_get_post( $id ); - - $this->assertEquals( $id, $test->get( 'id' ) ); - $this->assertEquals( 'llms_order', $test->get( 'type' ) ); - $this->assertEquals( 'Order – Apr 22, 2021 @ 02:34 PM', $test->get( 'title' ) ); - - $this->assertEquals( $date, $test->get( 'date' ) ); - $this->assertEquals( 'llms-pending', $test->get( 'status' ) ); - - $this->assertTrue( post_password_required( $id ) ); - - } - - - /** - * Overrides test from the abstract - * - * Since this test isn't essential for this class we'll skip the test with a fake assertion - * in order to reduce the number of skipped tests warnings which are output. - * - * @since 3.37.6 - * - * @return void - */ - public function test_edit_date() { - $this->assertTrue( true ); - } - - /** - * Test the generate_order_key() method - * - * @since 3.10.0 - * - * @return void - */ - public function test_generate_order_key() { - - $this->assertTrue( is_string( $this->obj->generate_order_key() ) ); - $this->assertEquals( 0, strpos( $this->obj->generate_order_key(), 'order-' ) ); - - } - - /** - * Test the get_access_expiration_date() method - * - * @since 3.10.0 - * @since 3.19.0 Unknown. - * - * @return void - */ - public function test_get_access_expiration_date() { - - // Lifetime responds with a string not a date. - $this->obj->set( 'access_expiration', 'lifetime' ); - $this->assertEquals( 'Lifetime Access', $this->obj->get_access_expiration_date() ); - - // Expires on a specific date. - $this->obj->set( 'access_expiration', 'limited-date' ); - $this->obj->set( 'access_expires', '12/01/2020' ); // m/d/Y format (from datepicker). - $this->assertEquals( '2020-12-01', $this->obj->get_access_expiration_date() ); - - // Expires after a period of time. - $this->obj->set( 'access_expiration', 'limited-period' ); - - $tests = array( - array( - 'start' => '05/25/2015', - 'length' => '1', - 'period' => 'week', - 'expect' => '06/01/2015', - ), - array( - 'start' => '12/21/2017', - 'length' => '1', - 'period' => 'day', - 'expect' => '12/22/2017', - ), - array( - 'start' => '02/05/2017', - 'length' => '1', - 'period' => 'year', - 'expect' => '02/05/2018', - ), - array( - 'start' => '12/31/2017', - 'length' => '1', - 'period' => 'day', - 'expect' => '01/01/2018', - ), - array( - 'start' => '05/01/2017', - 'length' => '2', - 'period' => 'month', - 'expect' => '07/01/2017', - ), - ); - - foreach ( $tests as $data ) { - - $this->obj->set( 'start_date', $data['start'] ); - $this->obj->set( 'access_length', $data['length'] ); - $this->obj->set( 'access_period', $data['period'] ); - $this->assertEquals( date_i18n( 'Y-m-d', strtotime( $data['expect'] ) ), $this->obj->get_access_expiration_date() ); - - } - - // Recurring pending cancel has access until the next payment due date. - $this->obj->set( 'order_type', 'recurring' ); - $this->obj->set( 'status', 'llms-pending-cancel' ); - $this->assertEquals( $this->obj->get_next_payment_due_date( 'U' ), $this->obj->get_access_expiration_date( 'U' ) ); - - } - - /** - * Test get access status function - * - * @since 3.10.0 - * @since 3.19.0 Unknown. - * - * @return void - */ - public function test_get_access_status() { - - $this->assertEquals( 'inactive', $this->obj->get_access_status() ); - - $this->obj->set( 'order_type', 'single' ); - $this->obj->set( 'status', 'llms-active' ); - $this->assertEquals( 'active', $this->obj->get_access_status() ); - - $this->obj->set( 'status', 'llms-completed' ); - $this->obj->set( 'access_expiration', 'lifetime' ); - $this->assertEquals( 'active', $this->obj->get_access_status() ); - - // Past should still grant access. - llms_mock_current_time( '2010-05-05' ); - $this->assertEquals( 'active', $this->obj->get_access_status() ); - - // Future should still grant access. - llms_mock_current_time( '2525-05-05' ); - $this->assertEquals( 'active', $this->obj->get_access_status() ); - - // Check limited access by date. - $this->obj->set( 'access_expiration', 'limited-date' ); - $tests = array( - array( - 'now' => '2010-05-05', - 'expires' => '05/06/2010', // m/d/Y from datepicker. - 'expect' => 'active', - ), - array( - 'now' => '2015-05-05', - 'expires' => '05/06/2010', // m/d/Y from datepicker. - 'expect' => 'expired', - ), - array( - 'now' => '2010-05-05', - 'expires' => '05/05/2010', // m/d/Y from datepicker. - 'expect' => 'active', - ), - array( - 'now' => '2010-05-06', - 'expires' => '05/05/2010', // m/d/Y from datepicker. - 'expect' => 'expired', - ), - ); - - foreach ( $tests as $data ) { - llms_mock_current_time( $data['now'] ); - $this->obj->set( 'access_expires', $data['expires'] ); - $this->assertEquals( $data['expect'], $this->obj->get_access_status() ); - if ( 'active' === $data['expect'] ) { - $this->assertTrue( $this->obj->has_access() ); - } else { - $this->assertFalse( $this->obj->has_access() ); - } - } - - $this->obj->set( 'order_type', 'recurring' ); - $this->obj->set( 'status', 'llms-pending-cancel' ); - $this->assertEquals( 'active', $this->obj->get_access_status() ); - - } - - /** - * Test the get_customer_name() method. - * - * @since Unknown - * @since 5.2.1 Add assertion for anonymized order. - * - * @return void - */ - public function test_get_customer_name() { - $first = 'Jeffrey'; - $last = 'Lebowski'; - $this->obj->set( 'billing_first_name', $first ); - $this->obj->set( 'billing_last_name', $last ); - $this->assertEquals( $first . ' ' . $last, $this->obj->get_customer_name() ); - - $this->obj->set( 'anonymized', 'yes' ); - $this->assertEquals( 'Anonymous', $this->obj->get_customer_name() ); - - } - - /** - * Test the get_gateway() method. - * - * @return void - */ - public function test_get_gateway() { - - // Gateway doesn't exist. - $this->obj->set( 'payment_gateway', 'garbage' ); - $this->assertTrue( is_a( $this->obj->get_gateway(), 'WP_Error' ) ); - - $manual = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - $this->obj->set( 'payment_gateway', 'manual' ); - - // Real gateway that's not enabled. - update_option( $manual->get_option_name( 'enabled' ), 'no' ); - $this->assertTrue( is_a( $this->obj->get_gateway(), 'WP_Error' ) ); - - // Enabled gateway responds with the gateway instance. - update_option( $manual->get_option_name( 'enabled' ), 'yes' ); - $this->assertTrue( is_a( $this->obj->get_gateway(), 'LLMS_Payment_Gateway_Manual' ) ); - - } - - /** - * Test get_initial_price() method - * - * @return void - */ - public function test_get_initial_price() { - - // No trial. - $order = $this->get_order(); - $this->assertEquals( 25.99, $order->get_initial_price( array(), 'float' ) ); - - // With trial. - $trial_plan = $this->get_plan( 25.99, 1, 'lifetime', false, true ); - $order = $this->get_order( $trial_plan ); - $this->assertEquals( 1.00, $order->get_initial_price( array(), 'float' ) ); - - } - - /** - * Test get_notes() method - * - * @return void - */ - public function test_get_notes() { - - $i = 1; - while( $i <= 10 ) { - - $this->obj->add_note( sprintf( 'note %d', $i ) ); - $i++; - - } - - // Remove filter so we can test order notes. - remove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) ); - - $notes = $this->obj->get_notes( 1, 1 ); - - $this->assertCount( 1, $notes ); - $this->assertTrue( is_a( $notes[0], 'WP_Comment' ) ); - - $notes_p_1 = $this->obj->get_notes( 5, 1 ); - $notes_p_2 = $this->obj->get_notes( 5, 2 ); - $this->assertCount( 5, $notes_p_1 ); - $this->assertCount( 5, $notes_p_2 ); - $this->assertTrue( $notes_p_2 !== $notes_p_1 ); - - add_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) ); - - } - - /** - * Test get_product() method - * - * @return void - */ - public function test_get_product() { - - $course = new LLMS_Course( 'new', 'test' ); - $this->obj->set( 'product_id', $course->get( 'id' ) ); - $this->assertTrue( is_a( $this->obj->get_product(), 'LLMS_Course' ) ); - - } - - // public function test_get_last_transaction() {} - - // public function test_get_last_transaction_date() {} - - /** - * Test get_next_payment_due_date() for a one-time payment - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_next_payment_due_date_single() { - - $plan = $this->get_plan( 25.99, 0 ); - $order = $this->get_order( $plan ); - $this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) ); - - } - - /** - * Test get_next_payment_due_date() for recurring payments - * - * @since Unknown. - * @since 3.37.6 Adjusted delta on date comparison to allow 2 hours difference when calculating recurring payment dates. - * @since 5.3.0 Don't rely on the date_billing_end property for ending a payment plan. - * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter provided to `assertEquals()`. - * - * @return void - */ - public function test_get_next_payment_due_date_recurring() { - - $original_time = current_time( 'Y-m-d H:i:s' ); - - $plan = $this->get_plan(); - foreach ( array( 'day', 'week', 'month', 'year' ) as $period ) { - - llms_mock_current_time( $original_time ); - - $plan->set( 'period', $period ); - - // Test due date with a trial. - $plan->set( 'trial_offer', 'yes' ); - $order = $this->get_order( $plan ); - $this->assertEqualsWithDelta( strtotime( $order->get_trial_end_date() ), strtotime( $order->get_next_payment_due_date() ), $this->date_delta ); - $plan->set( 'trial_offer', 'no' ); - - // Perform calculation tests against different frequencies. - $i = 1; - while ( $i <= 3 ) { - - $plan->set( 'frequency', $i ); - - $order = $this->get_order( $plan ); - - $expect = strtotime( "+{$i} {$period}", $order->get_date( 'date', 'U' ) ); - $this->assertEquals( $expect, $order->get_next_payment_due_date( 'U') ); - - // Time travel a bit and recalculate the time. - llms_mock_current_time( date( 'Y-m-d H:i:s', $expect + HOUR_IN_SECONDS * 2 ) ); - $future_expect = strtotime( "+{$i} {$period}", $expect ); - - // This will calculate the next payment date based off of the saved next payment date (which is now in the past). - $order->maybe_schedule_payment( true ); - $this->assertEquals( $future_expect, $order->get_next_payment_due_date( 'U' ) ); - - // Recalculate off a transaction -- this is the fallback for pre 3.10 orders. - // Occurs only when no date_next_payment is set. - $order->set( 'date_next_payment', '' ); - $order->record_transaction( array( - 'amount' => 25.99, - 'completed_date' => $original_time, - 'status' => 'llms-txn-succeeded', - 'payment_type' => 'recurring', - ) ); - $order->maybe_schedule_payment( true ); - - $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 ); - - // Plan ended so func should return a WP_Error. - $order->set( 'billing_length', 1 ); - $order->maybe_schedule_payment( true ); - $date = $order->get_next_payment_due_date(); - $this->assertIsWPError( $date ); - $this->assertWPErrorCodeEquals( 'plan-ended', $date ); - $this->assertEquals( 'yes', $order->get( 'plan_ended' ) ); - $order->set( 'billing_length', 0 ); - - $i++; - - } - - } - - } - - /** - * Test get_next_payment_due_date() method for a payment plan - * - * Additionally tests calculate_next_payment_date() via action hooks. - * - * @since Unknown - * @since 5.3.0 Updated to rely on number of successful transactions in favor of the current date. - * - * @return void - */ - public function test_get_next_payment_due_date_payment_plan() { - - $original_time = current_time( 'Y-m-d H:i:s' ); - - llms_mock_current_time( $original_time ); - - // This should run 3 total payments over the course of 9 weeks. - $plan = $this->get_plan(); - $plan->set( 'frequency', 3 ); // Every 3rd. - $plan->set( 'period', 'week' ); // Week. - $plan->set( 'length', 3 ); // For 3 payments. - - // Create the order. - $order = $this->get_order( $plan ); - - // 3 total payments due. - $this->assertEquals( 3, $order->get_remaining_payments() ); - - // Make the initial payment. - $order->record_transaction( array( - 'payment_type' => 'recurring', - 'status' => 'llms-txn-succeeded', - ) ); - - // Two payments remaining. - $this->assertEquals( 2, $order->get_remaining_payments() ); - - // Payment two is scheduled properly. - $expect = strtotime( "+3 weeks", $order->get_date( 'date', 'U' ) ); - $this->assertEquals( $expect, $order->get_next_payment_due_date( 'U' ) ); - - // Time travel to when the second payment is due. - llms_mock_current_time( date( 'Y-m-d H:i:s', $expect ) ); - - // Record the second payment. - $order->record_transaction( array( - 'payment_type' => 'recurring', - 'status' => 'llms-txn-succeeded', - ) ); - - // Only one payment remaining. - $this->assertEquals( 1, $order->get_remaining_payments() ); - - // Payment 3 is scheduled properly. - $expect += WEEK_IN_SECONDS * 3; - $this->assertEquals( $expect, $order->get_next_payment_due_date( 'U' ) ); - - // Time travel to when the 3rd payment is due. - llms_mock_current_time( date( 'Y-m-d H:i:s', $expect ) ); - - // Make the 3rd payment. - $order->record_transaction( array( - 'payment_type' => 'recurring', - 'status' => 'llms-txn-succeeded', - ) ); - - // No more payments due. - $this->assertTrue( is_a( $order->get_next_payment_due_date( 'U' ), 'WP_Error' ) ); - $this->assertEquals( 0, $order->get_remaining_payments() ); - - } - - /** - * Test get_remaining_payments() - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_remaining_payments() { - - // Not recurring. - $this->assertFalse( $this->obj->get_remaining_payments() ); - - // No length. - $this->obj->set( 'order_type', 'recurring' ); - $this->assertFalse( $this->obj->get_remaining_payments() ); - - // Has length. - $this->obj->set( 'billing_length', 5 ); - $this->assertEquals( 5, $this->obj->get_remaining_payments() ); - - // These statuses don't count. - foreach ( array( 'failed', 'pending' ) as $status ) { - $this->obj->record_transaction( array( - 'status' => "llms-txn-{$status}", - 'payment_type' => 'recurring', - ) ); - $this->assertEquals( 5, $this->obj->get_remaining_payments() ); - } - - // Record a few successes. - $i = 1; - while ( $i <= 4 ) { - $this->obj->record_transaction( array( - 'payment_type' => 'recurring', - ) ); - $this->assertEquals( 5 - $i, $this->obj->get_remaining_payments(), $i ); - ++$i; - } - - // Refunds count? - $this->obj->record_transaction( array( - 'status' => 'llms-txn-refunded', - 'payment_type' => 'recurring', - ) ); - $this->assertEquals( 0, $this->obj->get_remaining_payments() ); - - } - - // public function test_get_transaction_total() {} - - // public function test_get_start_date() {} - - // public function test_get_transactions() {} - - /** - * Test get_trial_end_deate() method - * - * @return void - */ - public function test_get_trial_end_date() { - - $this->obj->set( 'order_type', 'recurring' ); - - // No trial so false for end date. - $this->assertEmpty( $this->obj->get_trial_end_date() ); - - // Enable trial. - $this->obj->set( 'trial_offer', 'yes' ); - $start = $this->obj->get_start_date( 'U' ); - - // When the date is saved the getter shouldn't calculate a new date and should return the saved date. - $set = '2017-05-05 13:42:19'; - $this->obj->set( 'date_trial_end', $set ); - $this->assertEquals( $set, $this->obj->get_trial_end_date() ); - $this->obj->set( 'date_trial_end', '' ); - - // Run a bunch of tests testing the dynamic calculations for various periods and whatever. - foreach ( array( 'day', 'week', 'month', 'year' ) as $period ) { - - $this->obj->set( 'trial_period', $period ); - $i = 1; - while ( $i <= 3 ) { - - llms_mock_current_time( date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) ); - - $this->obj->set( 'trial_length', $i ); - $expect = strtotime( '+' . $i . ' ' . $period, $start ); - $this->assertEquals( $expect, $this->obj->get_trial_end_date( 'U' ) ); - - // Trial is not over. - $this->assertFalse( $this->obj->has_trial_ended() ); - - // Change date to future. - llms_mock_current_time( date( 'Y-m-d H:i:s', $this->obj->get_trial_end_date( 'U' ) + HOUR_IN_SECONDS ) ); - $this->assertTrue( $this->obj->has_trial_ended() ); - - $i++; - - } - - } - - } - - // public function test_get_revenue() {} - - /** - * Test has_coupon() method - * - * @return void - */ - public function test_has_coupon() { - - $this->obj->set( 'coupon_used', 'whatarst' ); - $this->assertFalse( $this->obj->has_coupon() ); - - $this->obj->set( 'coupon_used', 'no' ); - $this->assertFalse( $this->obj->has_coupon() ); - - $this->obj->set( 'coupon_used', '' ); - $this->assertFalse( $this->obj->has_coupon() ); - - $this->obj->set( 'coupon_used', 'yes' ); - $this->assertTrue( $this->obj->has_coupon() ); - - } - - /** - * Test had_discount() method - * - * @return void - */ - public function test_has_discount() { - - $this->obj->set( 'coupon_used', 'yes' ); - $this->assertTrue( $this->obj->has_discount() ); - - $this->obj->set( 'coupon_used', 'no' ); - $this->assertFalse( $this->obj->has_discount() ); - - $this->obj->set( 'on_sale', 'yes' ); - $this->assertTrue( $this->obj->has_discount() ); - - $this->obj->set( 'on_sale', 'no' ); - $this->assertFalse( $this->obj->has_discount() ); - - } - - /** - * Test has_plan_expiration() - * - * @since 5.3.0 - * - * @return void - */ - public function test_has_plan_expiration() { - - // Single payment. - $this->assertFalse( $this->obj->has_plan_expiration() ); - - // Recurring with no length. - $this->obj->set( 'order_type', 'recurring' ); - $this->assertFalse( $this->obj->has_plan_expiration() ); - - // Has length. - $this->obj->set( 'billing_length', 1 ); - $this->assertTrue( $this->obj->has_plan_expiration() ); - - } - - /** - * Test has_sale() method - * - * @return void - */ - public function test_has_sale() { - - $this->obj->set( 'on_sale', 'whatarst' ); - $this->assertFalse( $this->obj->has_sale() ); - - $this->obj->set( 'on_sale', 'no' ); - $this->assertFalse( $this->obj->has_sale() ); - - $this->obj->set( 'on_sale', '' ); - $this->assertFalse( $this->obj->has_sale() ); - - $this->obj->set( 'on_sale', 'yes' ); - $this->assertTrue( $this->obj->has_sale() ); - - } - - // public function test_has_scheduled_payment() {} - - /** - * Test has_trial() method - * - * @return void - */ - public function test_has_trial() { - - $this->obj->set( 'order_type', 'recurring' ); - - $this->obj->set( 'trial_offer', 'whatarst' ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'trial_offer', 'no' ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'trial_offer', '' ); - $this->assertFalse( $this->obj->has_trial() ); - - $this->obj->set( 'trial_offer', 'yes' ); - $this->assertTrue( $this->obj->has_trial() ); - - } - - /** - * Test init() with a plan that has a trial. - * - * @since Unknown - * - * @return void - */ - public function test_init_with_trial() { - - // Test initialization of a trial. - $plan = $this->get_plan( 25.99, 1, 'lifetime', false, true ); - $order = $this->get_order( $plan ); - - $this->assertTrue( $order->has_trial() ); - $this->assertNotEmpty( $order->get( 'date_trial_end' ) ); - - - } - - /** - * Test the is_recurring() method. - * - * @since Unknown - * - * @return void - */ - public function test_is_recurring() { - - $this->assertFalse( $this->obj->is_recurring() ); - $this->obj->set( 'order_type', 'recurring' ); - $this->assertTrue( $this->obj->is_recurring() ); - - } - - /** - * Test the schedule expiration function - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`. - * - * @return void - */ - public function test_maybe_schedule_expiration() { - - // Recurring order with lifetime access won't schedule expiration. - $order = $this->get_mock_order(); - - $order->set_status( 'llms-active' ); - $order->maybe_schedule_expiration(); - - $this->assertFalse( as_next_scheduled_action( 'llms_access_plan_expiration', array( - 'order_id' => $order->get( 'id' ), - ) ) ); - $this->assertFalse( $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) ); - - // Limited access will schedule expiration. - $plan = $this->get_mock_plan( '25.99', 0, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - - $order->set_status( 'llms-active' ); - $order->maybe_schedule_expiration(); - - $action_time = as_next_scheduled_action( 'llms_access_plan_expiration', array( - 'order_id' => $order->get( 'id' ), - ) ); - $this->assertEquals( $order->get_access_expiration_date( 'U' ), $action_time ); - $this->assertEquals( $action_time, $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) ); - - } - - /** - * Test recurring payment scheduling for a one-time order - * - * @since 4.7.0 Split from test_maybe_schedule_payment_recurring() - * - * @return void - */ - public function test_maybe_schedule_payment_one_time() { - - // Does nothing for a one-time order. - $plan = $this->get_mock_plan( '25.99', 0 ); - $order = $this->get_mock_order( $plan ); - $order->maybe_schedule_payment(); - $this->assertEmpty( $order->get( 'date_next_payment' ) ); - - } - - /** - * Test recurring payment scheduling for a recurring order - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`. - * @since 4.7.0 Split into its own method to prevent variable clashes. - * - * @return void - */ - public function test_maybe_schedule_payment_recurring() { - - $order = $this->get_mock_order(); - - $this->assertFalse( as_next_scheduled_action( 'llms_charge_recurring_payment', array( - 'order_id' => $order->get( 'id' ), - ) ) ); - $this->assertFalse( $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ); - - $order->maybe_schedule_payment(); - $this->assertTrue( ! empty( $order->get( 'date_next_payment' ) ) ); - - $action_time = as_next_scheduled_action( 'llms_charge_recurring_payment', array( 'order_id' => $order->get( 'id' ) ) ); - $this->assertEquals( $order->get_next_payment_due_date( 'U' ), $action_time ); - $this->assertEquals( $action_time, $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ); - - } - - /** - * Test maybe_schedule_retry() method. - * - * @return void - */ - public function test_maybe_schedule_retry() { - - $this->mock_gateway_support( 'recurring_retry' ); - - $order = $this->get_order(); - $order->set_status( 'on-hold' ); - - $i = 1; - while ( $i <= 5 ) { - - $original_next_date = $order->get_next_payment_due_date( 'U' ); - - $txn = $order->record_transaction( array( - 'amount' => 25.99, - 'status' => 'llms-txn-pending', - 'payment_type' => 'recurring', - ) ); - $txn->set( 'status', 'llms-txn-failed' ); - - $order = llms_get_post( $order->get( 'id' ) ); - - if ( $i <= 4 ) { - - $this->assertEquals( $i, did_action( 'llms_automatic_payment_retry_scheduled' ) ); - $this->assertEquals( $i - 1, $order->get( 'last_retry_rule' ) ); - $this->assertNotEquals( $original_next_date, $order->get_next_payment_due_date( 'U' ) ); - - } else { - - $this->assertEquals( 1, did_action( 'llms_automatic_payment_maximum_retries_reached' ) ); - $this->assertEquals( '', $order->get( 'last_retry_rule' ) ); - $this->assertEquals( 'llms-failed', $order->get( 'status' ) ); - - } - - - $i++; - - } - - } - - /** - * Test record_transaction() method - * - * @since Unknown - * - * @return void - */ - public function test_record_transaction() { - - $order = $this->get_order(); - $txn = $order->record_transaction( array( - 'amount' => 25.99, - 'status' => 'llms-txn-succeeded', - 'payment_type' => 'recurring', - ) ); - $this->assertTrue( is_a( $txn, 'LLMS_Transaction' ) ); - $order = llms_get_post( $order->get( 'id' ) ); - $this->assertEquals( 'llms-active', $order->get( 'status' ) ); - $this->assertEquals( 1, did_action( 'lifterlms_transaction_status_succeeded' ) ); - $this->assertEquals( 1, did_action( 'lifterlms_order_status_active' ) ); - - } - - /** - * Test the set_date() method - * - * @since 3.19.0 - * - * @return void - */ - public function test_set_date() { - - $dates = array( - 'next_payment', - 'trial_end', - 'access_expires', - ); - - foreach ( $dates as $key ) { - - // Set via date string. - $date = current_time( 'mysql' ); - $this->obj->set_date( $key, $date ); - $this->assertEquals( $date, $this->obj->get( 'date_' . $key ) ); - - // Set via timestamp. - $timestamp = current_time( 'timestamp' ); - $this->obj->set_date( $key, $timestamp ); - $this->assertEquals( date_i18n( 'Y-m-d H:i:s', $timestamp ), $this->obj->get( 'date_' . $key ) ); - - } - - } - - /** - * Test set_status() method - * - * @since Unknown - * - * @return void - */ - public function test_set_status() { - - $this->obj->set_status( 'fakestatus' ); - $this->assertNotEquals( 'fakestatus', $this->obj->get( 'status' ) ); - - $this->obj->set( 'order_type', 'single' ); - foreach ( array_keys( llms_get_order_statuses( 'single' ) ) as $status ) { - - $this->obj->set_status( $status ); - $this->assertEquals( $status, $this->obj->get( 'status' ) ); - - $unprefixed = str_replace( 'llms-', '', $status ); - $this->obj->set_status( $unprefixed ); - $this->assertEquals( $status, $this->obj->get( 'status' ) ); - - } - - } - - /** - * Test the start access method - * - * @since 3.19.0 - * @since 3.32.0 Update to use latest action-scheduler functions. - * - * @return void - */ - public function test_start_access() { - - $plan = $this->get_mock_plan( '25.99', 0, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - - // Freeze time. - $time = current_time( 'mysql' ); - llms_mock_current_time( $time ); - - // Prior to starting access there should be no access start date. - $this->assertEmpty( $order->get( 'start_date' ) ); - - // Start the access. - $order->start_access(); - - // Time should be our mocked time. - $this->assertEquals( $time, $order->get( 'start_date' ) ); - - // An expiration event should be scheduled to match the expiration date. - $event_time = as_next_scheduled_action( 'llms_access_plan_expiration', array( - 'order_id' => $order->get( 'id' ), - ) ); - $this->assertEquals( $order->get_access_expiration_date( 'U' ), $event_time ); - - } - - /** - * Test unschedule_expiration() method - * - * @since 4.6.0 - * - * @return void - */ - public function test_unschedule_expiration() { - - $plan = $this->get_mock_plan( '25.99', 0, 'limited-date' ); - $order = $this->get_mock_order( $plan ); - - $order->set_status( 'llms-active' ); - $order->maybe_schedule_expiration(); - - $order->unschedule_expiration(); - - $this->assertFalse( $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) ); - - } - - /** - * Test unschedule_recurring_payment() method - * - * @since 4.6.0 - * - * @return void - */ - public function test_unschedule_recurring_payment() { - - $order = $this->get_mock_order(); - $order->maybe_schedule_payment(); - - $order->unschedule_recurring_payment(); - - $this->assertFalse( $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ); - - } - - /** - * Test get_customer_full_address() method - * - * @since 5.2.0 - * - * @return void - */ - public function test_get_customer_full_address() { - - $customer_details = array( - 'billing_address_1' => 'Rue Jennifer 7', - 'billing_address_2' => 'c/o Juniper', - 'billing_city' => 'Pasadena', - 'billing_state' => 'CA', - 'billing_zip' => '28282', - 'billing_country' => 'US' - ); - - $this->obj->set_bulk( $customer_details ); - - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena CA, 28282, United States', $this->obj->get_customer_full_address() ); - - // Remove city. - $this->obj->set( 'billing_city', '' ); - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, CA, 28282, United States', $this->obj->get_customer_full_address() ); - - // Remove state. - $this->obj->set( 'billing_state', '' ); - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, 28282, United States', $this->obj->get_customer_full_address() ); - - // Add back city. - $this->obj->set( 'billing_city', $customer_details['billing_city'] ); - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena, 28282, United States', $this->obj->get_customer_full_address() ); - - // Remove zip code. - $this->obj->set( 'billing_zip', '' ); - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena, United States', $this->obj->get_customer_full_address() ); - - // Remove country. - $this->obj->set( 'billing_country', '' ); - $this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena', $this->obj->get_customer_full_address() ); - - // Remove secondary address. - $this->obj->set( 'billing_address_2', '' ); - $this->assertEquals( 'Rue Jennifer 7, Pasadena', $this->obj->get_customer_full_address() ); - - // Remove main billing address. We expect that nothing is returned. - $this->obj->set( 'billing_address_1', '' ); - $this->assertEquals( '', $this->obj->get_customer_full_address() ); - - } - - - /** - * Test get_recurring_payment_due_date_for_scheduler() method - * - * @since 5.2.0 - * - * @return void - */ - public function test_get_recurring_payment_due_date_for_scheduler() { - - $order = $this->get_mock_order(); - - $now = current_time( 'timestamp' ); - llms_mock_current_time( $now ); - - // One time payment plan. - $plan = $this->get_plan( '25.99', 0 ); - - $order = $this->get_mock_order( $plan ); - - $this->assertWPErrorCodeEquals( 'not-recurring', $order->get_recurring_payment_due_date_for_scheduler() ); - - // Check order with invalid status. - $plan = $this->get_plan(); - - $plan->set( 'frequency', 1 ); // Every. - $plan->set( 'period', 'month' ); // Month. - $plan->set( 'length', 3 ); // for 3 total payments. - $order = $this->get_mock_order( $plan ); - - $original_status = $order->get( 'status' ); - $order->set( 'status', 'some-invalid' ); - $this->assertWPErrorCodeEquals( 'invalid-status', $order->get_recurring_payment_due_date_for_scheduler() ); - $order->set( 'status', $original_status); - - // Check providing a (boolean) false date. - $this->assertWPErrorCodeEquals( 'invalid-recurring-payment-date', $order->get_recurring_payment_due_date_for_scheduler( 0 ) ); - - // Check the returning timestamp is the order next payment due date converted to UTC. - $this->assertEquals( - get_gmt_from_date( $order->get_next_payment_due_date(), 'U' ), - $order->get_recurring_payment_due_date_for_scheduler() - ); - - // Pretend we the next payment due date was UTC. - $this->assertEquals( - date_format( date_create( $order->get_next_payment_due_date() ), 'U' ), - $order->get_recurring_payment_due_date_for_scheduler( false, true ) - ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-product.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-product.php deleted file mode 100644 index e7854df10c..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-product.php +++ /dev/null @@ -1,535 +0,0 @@ -<?php -/** - * Tests for LifterLMS Product Model - * - * @package LifterLMS/Tests/Models - * - * @group LLMS_Product - * @group LLMS_Post_Model - * - * @since 3.25.2 - * @since 3.37.12 Create a stub for the test_create_method() since this class doesn't need to test that. - * @since 3.38.0 Add tests for the get_restrictions() and has_restrictions() methods. - * Override unnecessary parent tests so they're not marked as skipped. - * @since 5.4.0 Added tests for `has_active_subscriptions` method. - */ -class LLMS_Test_LLMS_Product extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class. - * - * @var string - */ - protected $class_name = 'LLMS_Product'; - - /** - * DB post type of the model being tested. - * - * @var string - */ - protected $post_type = 'product'; - - /** - * Get properties, used by test_getters_setters. - * - * This should match, exactly, the object's $properties array. - * - * @since 3.24.0 - * - * @return array - */ - protected function get_properties() { - return array(); - } - - /** - * Get data used to create the mock post. - * - * This is used by test_getters_setters(). - * - * @since 3.24.0 - * - * @return array - */ - protected function get_data() { - return array(); - } - - private function add_plan( $product, $data = array() ) { - - $data = wp_parse_args( $data, array( - 'title' => 'mock plan', - 'is_free' => 'no', - 'price' => 100.00, - 'frequency' => 0, - 'visibility' => 'visible', - ) ); - - $plan = new LLMS_Access_Plan( 'new', $data['title'] ); - - $plan->set( 'product_id', $product->get( 'id' ) ); - $plan->set_visibility( $data['visibility'] ); - - unset( $data['title'] ); - unset( $data['visibility'] ); - - foreach ( $plan->get_properties() as $prop => $type ) { - - if ( array_key_exists( $prop, $data ) ) { - $plan->set( $prop, $data[ $prop ] ); - } elseif ( 'yesno' === $type ) { - $plan->set( $prop, 'no' ); - } - } - - return $plan; - - } - - private function get_product() { - - $product = new LLMS_Product( $this->factory->post->create( array( 'post_type' => 'course' ) ) ); - return $product; - - } - - /** - * Override parent test. - * - * This model has no properties of it's own so we can safely skip this test - * without outputting a warning. - * - * @since 3.38.0 - * - * @return void - */ - public function test_getters_setters() { - $this->assertTrue( true ); - } - - - /** - * Overwrite parent class method that tests model creation. - * - * This model shouldn't be created, instead the `LLMS_Course` or `LLMS_Membership` classes are used to create products. - * - * @since 3.37.12 - * - * @return void - */ - public function test_create_model() { - $this->assertTrue( true ); - } - - - /** - * Overwrite unnecessary parent test. - * - * @since 3.38.0 - * - * @return void - */ - public function test_date_status_relationship_update() { - $this->assertTrue( true ); - } - - /** - * Overwrite unnecessary parent test. - * - * @since 3.38.0 - * - * @return void - */ - public function test_edit_date() { - $this->assertTrue( true ); - } - - /** - * Overwrite unnecessary parent test. - * - * @since 3.38.0 - * - * @return void - */ - public function test_set_bulk() { - $this->assertTrue( true ); - } - - /** - * Overwrite unnecessary parent test. - * - * @since 3.38.0 - * - * @return void - */ - public function test_set_bulk_wp_error() { - $this->assertTrue( true ); - } - - - /** - * Test get_access_plan_limit() method. - * - * @since 3.25.2 - * @since 5.4.0 Remove 'llms_get_product_access_plan_limit' filter callback after use. - * - * @return void - */ - public function test_get_access_plan_limit() { - - $product = $this->get_product(); - - $this->assertTrue( is_int( $product->get_access_plan_limit() ) ); - $this->assertEquals( 6, $product->get_access_plan_limit() ); - - // Test the filter. - $return_three = function() { - return 3; - }; - - add_filter( 'llms_get_product_access_plan_limit', $return_three ); - $this->assertEquals( 3, $product->get_access_plan_limit() ); - remove_filter( 'llms_get_product_access_plan_limit', $return_three ); - - } - - /** - * Test get_access_plan_limit() method. - * - * @since Unknown. - * - * @return void - */ - public function test_get_access_plans() { - - $product = $this->get_product(); - - // No plans. - $this->assertEquals( 0, count( $product->get_access_plans() ) ); - - // Add a plan. - $this->add_plan( $product ); - // One plan returned. - $this->assertEquals( 1, count( $product->get_access_plans() ) ); - - // Add a free plan. - $this->add_plan( $product, array( 'is_free' => 'yes' ) ); - // Two plans returned. - $this->assertEquals( 2, count( $product->get_access_plans() ) ); - // Exclude free. - $this->assertEquals( 1, count( $product->get_access_plans( true ) ) ); - - // Add a hidden plan. - $this->add_plan( $product, array( 'visibility' => 'hidden' ) ); - // Show all plans except hidden. - $this->assertEquals( 2, count( $product->get_access_plans() ) ); - // Only show free & visible plans. - $this->assertEquals( 1, count( $product->get_access_plans( true ) ) ); - // Show all plans. - $this->assertEquals( 3, count( $product->get_access_plans( false, false ) ) ); - // Only show free (allow hidden plans). - $this->assertEquals( 1, count( $product->get_access_plans( true, false ) ) ); - - } - - /** - * Test get_restrictions(): no restrictions on product. - * - * @since 3.38.0 - * - * @return void - */ - public function test_get_restrictions_none() { - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - $this->assertEquals( array(), $product->get_restrictions() ); - - } - - /** - * Test get_restrictions(): enrollment period. - * - * @since 3.38.0 - * - * @return void - */ - public function test_get_restrictions_period() { - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) ); - $this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() ); - - } - - /** - * Test get_restrictions(): max capacity. - * - * @since 3.38.0 - * - * @return void - */ - public function test_get_restrictions_capacity() { - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - $course->set( 'enable_capacity', 'yes' ); - $course->set( 'capacity', 1 ); - - $this->assertEquals( array( 'student_capacity' ), $product->get_restrictions() ); - - } - - /** - * Test get_restrictions(): multiple restrictions. - * - * @since 3.38.0 - * - * @return void - */ - public function test_get_restriction_multiple() { - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - // Enrollment period. - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) ); - $this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() ); - - // No capacity. - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - $course->set( 'enable_capacity', 'yes' ); - $course->set( 'capacity', 1 ); - - $this->assertEquals( array( 'enrollment_period', 'student_capacity' ), $product->get_restrictions() ); - } - - /** - * Test has_free_access_plan() method. - * - * @since 3.25.2 - * - * @return void - */ - public function test_has_free_access_plan() { - - $product = $this->get_product(); - - // Has no plans. - $this->assertFalse( $product->has_free_access_plan() ); - - // Has paid plan. - $this->add_plan( $product ); - $this->assertFalse( $product->has_free_access_plan() ); - - $this->add_plan( $product, array( 'is_free' => 'yes' ) ); - $this->assertTrue( $product->has_free_access_plan() ); - - } - - /** - * Test the has_restrictions() method. - * - * @since 3.38.0 - * - * @return void - */ - public function test_has_restrictions() { - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - // No restrictions. - $this->assertFalse( $product->has_restrictions() ); - - // Add enrollment period restrictions. - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) ); - $this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() ); - - $this->assertTrue( $product->has_restrictions() ); - - } - - /** - * Test the is_purchasable() method. - * - * @since 3.25.2 - * - * @return void - */ - public function test_is_purchasable() { - - $manual = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $manual->get_option_name( 'enabled' ), 'no' ); - - $product = $this->get_product(); - $course = llms_get_post( $product->get( 'id' ) ); - - // Enrollment is closed. - $course->set( 'enrollment_period', 'yes' ); - $course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) ); - $this->assertFalse( $product->is_purchasable() ); - $course->set( 'enrollment_period', 'no' ); - - // No capacity. - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - $course->set( 'enable_capacity', 'yes' ); - $course->set( 'capacity', 1 ); - $this->assertFalse( $product->is_purchasable() ); - - // Enrollment closed & no capacity. - $course->set( 'enrollment_period', 'yes' ); - $this->assertFalse( $product->is_purchasable() ); - $course->set( 'enable_capacity', 'no' ); - $course->set( 'enrollment_period', 'no' ); - - // No plans & no gateways. - $this->assertFalse( $product->is_purchasable() ); - - // Has a plan but no gateway. - $plan = $this->add_plan( $product ); - $this->assertFalse( $product->is_purchasable() ); - - // Has a plan and a gateway. - update_option( $manual->get_option_name( 'enabled' ), 'yes' ); - $this->assertTrue( $product->is_purchasable() ); - - // Only free plans. - $plan->set( 'is_free', 'yes' ); - $this->assertTrue( $product->is_purchasable() ); - - // No plans but has a gateway. - wp_delete_post( $plan->get( 'id' ), true ); - $this->assertFalse( $product->is_purchasable() ); - - } - - /** - * Test `has_active_subscriptions()` on a product only related to a single order. - * - * @since 5.4.0 - * - * @return void - */ - public function test_has_active_subscriptions_single_order() { - - $order = $this->get_mock_order(); - $product = $this->get_product(); - $order->set( 'product_id', $product->get('id') ); - $order->set( 'order_type', 'single' ); - - $this->assertFalse( $product->has_active_subscriptions( false ) ); - - $order->set( 'status', 'llms-active' ); - - $this->assertFalse( $product->has_active_subscriptions( false ) ); - - } - - /** - * Test `has_active_subscriptions()` on a product related to a single order and to an active subscription. - * - * @since 5.4.0 - * - * @return void - */ - public function test_has_active_subscriptions_single_and_recurring_order() { - - $order = $this->get_mock_order(); - $product = $this->get_product(); - - // Single order. - $order->set( 'product_id', $product->get('id') ); - $order->set( 'order_type', 'single' ); - - // Recurring order. - $order_recurring = $this->get_mock_order(); - $order_recurring->set( 'product_id', $product->get('id') ); - - foreach ( array( 'active', 'pending-cancel', 'on-hold' ) as $status ) { - $order_recurring->set( 'status', "llms-{$status}" ); - $this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) ); - } - - } - - /** - * Test `has_active_subscriptions()` on a product related to a single order and to a not active subscription. - * - * @since 5.4.0 - * - * @return void - */ - public function test_has_active_subscriptions_single_and_recurring_order_inactive_status() { - - $order = $this->get_mock_order(); - $product = $this->get_product(); - - // Single order. - $order->set( 'product_id', $product->get('id') ); - $order->set( 'order_type', 'single' ); - - // Recurring order. - $order_recurring = $this->get_mock_order(); - $order_recurring->set( 'product_id', $product->get('id') ); - - foreach ( array_keys( llms_get_order_statuses() ) as $status ) { - - if ( in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ) { - continue; - } - - $order_recurring->set( 'status', $status ); - $this->assertFalse( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) ); - - } - - } - - /** - * Test `has_active_subscriptions()` using cache mechanism. - * - * @since 5.4.0 - * - * @return void - */ - public function test_has_active_subscriptions_use_cache() { - - $order = $this->get_mock_order(); - $product = $this->get_product(); - - // Recurring order. - $order_recurring = $this->get_mock_order(); - $order_recurring->set( 'product_id', $product->get('id') ); - $order_recurring->set( 'status', 'llms-active' ); - - $this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) ); - - // Cancel subscription. - $order_recurring->set( 'status', 'llms-cancelled' ); - // Use cache, I expect an active subscription. - $this->assertTrue( $product->has_active_subscriptions( true ), $order_recurring->get( 'status' ) ); - // Do not use cache, I expect no active subscriptions. - $this->assertFalse( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) ); - - // Activate it again. - $order_recurring->set( 'status', 'llms-active' ); - // Use cache, I expect no active subscriptions. - $this->assertFalse( $product->has_active_subscriptions( true ), $order_recurring->get( 'status' ) ); - // Do not use cache, I expect an active subscription. - $this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-question.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-question.php deleted file mode 100644 index 9c0e618b24..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-question.php +++ /dev/null @@ -1,386 +0,0 @@ -<?php -/** - * Tests for LifterLMS Quiz Model - * - * @package LifterLMS_Tests/Models - * - * @group post_models - * @group quizzes - * @group questions - * - * @since 3.16.12 - * @since 3.30.1 Added more tests for `get_next_choice_marker()` and `get_choices()` - * @since 4.4.0 Add tests for the `grade()` method. - */ -class LLMS_Test_LLMS_Question extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_Question'; - - /** - * DB post type of the model being tested - * - * @var string - */ - protected $post_type = 'llms_question'; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array - * - * @since 3.16.12 - * - * @return array - */ - protected function get_properties() { - return array( - 'clarifications' => 'html', - 'clarifications_enabled' => 'yesno', - 'description_enabled' => 'yesno', - // 'image' => 'array', - 'multi_choices' => 'yesno', - 'parent_id' => 'absint', - 'points' => 'absint', - 'question_type' => 'string', - 'title' => 'html', - 'video_enabled' => 'yesno', - 'video_src' => 'string', - ); - } - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters - * - * @since 3.16.12 - * - * @return array - */ - protected function get_data() { - return array( - 'clarifications' => '<p>this is <b>a</b> clarification</p>', - 'clarifications_enabled' => 'yes', - 'description_enabled' => 'yes', - // 'image' => 'array', - 'multi_choices' => 'no', - 'parent_id' => 123, - 'points' => 3, - 'question_type' => 'choice', - 'title' => 'this <b>is</b> <i>a</i> question', - 'video_enabled' => 'yes', - 'video_src' => 'http://example.tld/video_embed', - ); - } - - /** - * Overwrite unnecessary parent test. - * - * @since 4.4.0 - * - * @return void - */ - public function test_edit_date() { - $this->assertTrue( true ); - } - - /** - * Test the has_description() method. - * - * @since 3.16.12 - * - * @return void - */ - public function test_has_description() { - - $this->create( 'title' ); - $this->assertFalse( $this->obj->has_description() ); - - $this->obj->set( 'content', 'arstarst' ); - $this->assertFalse( $this->obj->has_description() ); - - $this->obj->set( 'description_enabled', 'yes' ); - $this->assertTrue( $this->obj->has_description() ); - - $this->obj->set( 'content', '' ); - $this->assertFalse( $this->obj->has_description() ); - - } - - /** - * Test the has_video() method. - * - * @since 3.16.12 - * - * @return void - */ - public function test_has_video() { - - $this->create( 'title' ); - $this->assertFalse( $this->obj->has_video() ); - - $this->obj->set( 'video_src', 'http://example.tld/video_embed' ); - $this->assertFalse( $this->obj->has_video() ); - - $this->obj->set( 'video_enabled', 'yes' ); - $this->assertTrue( $this->obj->has_video() ); - - $this->obj->set( 'video_src', '' ); - $this->assertFalse( $this->obj->has_video() ); - - } - - /** - * Test the get_next_choice_marker() method. - * - * @since 3.30.1 - * - * @return void - */ - public function test_get_next_choice_marker() { - - $this->create(); - $this->obj->set( 'question_type', 'choice' ); - foreach( range( 'A', 'Z' ) as $expected ) { - $this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->obj, 'get_next_choice_marker' ) ); - $choice = $this->obj->get_choice( $this->obj->create_choice( array() ) ); - $this->assertEquals( $expected, $choice->get( 'marker' ) ); - } - - } - - /** - * Test the get_choices() method. - * - * @since 3.30.1 - * - * @return void - */ - public function test_get_choices() { - - foreach ( array( range( 'A', 'Z' ), range( 1, 26 ) ) as $i => $markers ) { - - $last_marker = false; - - if ( 1 === $i ) { - // Filter marker for testing numeric values. - add_filter( 'llms_get_question_type', function( $data, $type ) { - if ( 'choice' === $type ) { - $data['choices']['markers'] = range( 1, 26 ); - } - return $data; - }, 923, 2 ); - } - - $this->create(); - $this->obj->set( 'question_type', 'choice' ); - - // No choices. - $this->assertSame( array(), $this->obj->get_choices() ); - - $expected_ids = array(); - foreach ( $markers as $marker ) { - $expected_ids[] = $this->obj->create_choice( array( 'marker' => $marker ) ); - } - - $choices = $this->obj->get_choices( 'choices' ); - $this->assertEquals( 26, count( $choices ) ); - foreach ( $choices as $choice ) { - - // Ensure the correct order. - if ( ! empty( $last_marker ) ) { - $this->assertEquals( -1, strnatcmp( $last_marker, $choice->get( 'marker' ) ), $last_marker . ':' . $choice->get( 'marker' ) ); - } - $last_marker = $choice->get( 'marker' ); - - // Must be a choice. - $this->assertTrue( is_a( $choice, 'LLMS_Question_Choice' ) ); - - // Make sure the ID exists in the array. - $this->assertTrue( in_array( $choice->get( 'id' ), $expected_ids, true ) ); - - } - - // Only ids. - $ids = $this->obj->get_choices( 'ids' ); - $this->assertSame( 26, count( $ids ) ); - foreach( $expected_ids as $id ) { - $this->assertTrue( in_array( '_llms_choice_' . $id, $ids, true ) ); - } - - } - - // Remove marker filter. - remove_all_filters( 'llms_get_question_type', 923 ); - - } - - /** - * Test grade() for a question with no points. - * - * @since 4.4.0 - * - * @return void - */ - public function test_grade_no_points() { - - $question = new LLMS_Question( 'new' ); - $this->assertNull( $question->grade( array( 1 ) ) ); - - } - - /** - * Test grade() when grading is handled by a filter from a 3rd party - * - * @since 4.4.0 - * - * @return void - */ - public function test_grade_custom() { - - $question = new LLMS_Question( 'new' ); - $question->set( 'question_type', 'custom_grading_test_type' ); - - add_filter( 'llms_custom_grading_test_type_question_pre_grade', function( $grade ) { - return 'yes'; - }, 529 ); - - $this->assertEquals( 'yes', $question->grade( array( 1 ) ) ); - - remove_all_filters( 'llms_custom_grading_test_type_question_pre_grade', 529 ); - - } - - /** - * Test grade() for a multiple choice question with multiple correct answers. - * - * @since 4.4.0 - * - * @return void - */ - public function test_grade_choices_multi() { - - $question = new LLMS_Question( 'new' ); - $question->set( 'question_type', 'choice' ); - $question->set( 'multi_choices', 'yes' ); - $question->set( 'points', 1 ); - - $choices = array( - $question->create_choice( array( 'choice' => 'A', 'correct' => true ) ), - $question->create_choice( array( 'choice' => 'B' ) ), - $question->create_choice( array( 'choice' => 'C', 'correct' => true ) ), - ); - - // Correct answer. - $this->assertEquals( 'yes', $question->grade( array( $choices[0], $choices[2] ) ) ); - - // Order doesn't matter. - $this->assertEquals( 'yes', $question->grade( array( $choices[2], $choices[0] ) ) ); - - // Various potential incorrect answers. - $this->assertEquals( 'no', $question->grade( array( $choices[0] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices[1] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices[2] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices[0], $choices[1] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices[1], $choices[2] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices[0], $choices[1], $choices[2] ) ) ); - - } - - /** - * Test grade() for a multiple choice with a single correct answer and true_false questions. - * - * @since 4.4.0 - * - * @return void - */ - public function test_grade_choices_single() { - - foreach ( array( 'choice', 'true_false' ) as $question_type ) { - - $question = new LLMS_Question( 'new' ); - $question->set( 'question_type', $question_type ); - $question->set( 'points', 1 ); - - $choices = array( - 'correct' => $question->create_choice( array( 'choice' => 'A', 'correct' => true ) ), - 'incorrect' => $question->create_choice( array( 'choice' => 'B' ) ), - ); - - $this->assertEquals( 'yes', $question->grade( array( $choices['correct'] ) ) ); - $this->assertEquals( 'no', $question->grade( array( $choices['incorrect'] ) ) ); - - } - - } - - /** - * Test grade() for a conditionally auto-graded question. - * - * @since 4.4.0 - * - * @return void - */ - public function test_grade_conditional() { - - $question = new LLMS_Question( 'new' ); - $question->set( 'question_type', 'fake_conditional_type' ); - $question->set( 'points', 1 ); - $question->set( 'auto_grade', 'yes' ); - $question->set( 'correct_value', 'This is correct.' ); - - // Mock Conditional grading enabled. - add_filter( 'llms_fake_conditional_type_question_supports', function( $ret, $feature, $option ) { - return ( 'grading' === $feature && 'conditional' === $option ); - }, 329, 3 ); - - // Correct answers, case sensitivity doesn't matter. - $this->assertEquals( 'yes', $question->grade( array( 'This is correct.' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'THIS IS CORRECT.' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'this is correct.' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'tHiS is coRrECt.' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'this IS correct.' ) ) ); - - // Incorrect. - $this->assertEquals( 'no', $question->grade( array( 'This is not correct.' ) ) ); - - // Case matters now. - add_filter( 'llms_quiz_grading_case_sensitive', '__return_true' ); - $this->assertEquals( 'yes', $question->grade( array( 'This is correct.' ) ) ); - $this->assertEquals( 'no', $question->grade( array( 'this is correct.' ) ) ); - remove_filter( 'llms_quiz_grading_case_sensitive', '__return_true' ); - - // Add an additional value. - $question->set( 'correct_value', 'one|TWO' ); - - // Correct. - $this->assertEquals( 'yes', $question->grade( array( 'one', 'TWO' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'ONE', 'two' ) ) ); - $this->assertEquals( 'yes', $question->grade( array( 'OnE', 'Two' ) ) ); - - // Incorrect. - $this->assertEquals( 'no', $question->grade( array( 'TWO' ) ) ); - $this->assertEquals( 'no', $question->grade( array( 'one' ) ) ); - $this->assertEquals( 'no', $question->grade( array( 'fake' ) ) ); - - // Incorrect, order matters. - $this->assertEquals( 'no', $question->grade( array( 'TWO', 'one' ) ) ); - - // Make case matter. - add_filter( 'llms_quiz_grading_case_sensitive', '__return_true' ); - $this->assertEquals( 'yes', $question->grade( array( 'one', 'TWO' ) ) ); - $this->assertEquals( 'no', $question->grade( array( 'One', 'Two' ) ) ); - $this->assertEquals( 'no', $question->grade( array( 'Two' ) ) ); - remove_filter( 'llms_quiz_grading_case_sensitive', '__return_true' ); - - // Unmock. - remove_all_filters( 'llms_blank_question_supports', 329 ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz-attempt.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz-attempt.php deleted file mode 100644 index 1046c25ed3..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz-attempt.php +++ /dev/null @@ -1,701 +0,0 @@ -<?php -/** - * Tests LLMS_Quiz_Attempt model. - * - * @group quizzes - * @group quiz_attempt - * - * @since 3.9.0 - * @since 3.17.4 Unknown. - * @since 4.0.0 Add tests for the answer_question() method. - * @since 4.2.0 Added tests for the get_siblings() method. - * Added tests on lesson completion status when deleting attempts. - * @since 5.3.0 Added tests on get_question_objects() when filtering out the removed questions. - */ -class LLMS_Test_Model_Quiz_Attempt extends LLMS_UnitTestCase { - - /** - * Get an initialized mock attempt - * - * @since 3.9.2 - * @since 3.16.11 Unknown. - * @since 4.2.0 Added uid and courses parameter. - * - * @param integer $num_questions Optional. Number of questions to add to the quiz. Default 5. - * @param integer $uid Optional. WordPress user id, if not passed a new user will be created. Default `null`. - * @param int[] $course Optional. Course id, if not passed a new mock course will be created. Default `null`. - * @return obj - */ - private function get_mock_attempt( $num_questions = 5, $uid = null, $course = null ) { - - $uid = $uid ? $uid : $this->factory->user->create(); - $courses = ! empty( $course ) ? array( $course ) : $this->generate_mock_courses( 1, 1, 1, 1, $num_questions ); - - $course = llms_get_post( $courses[0] ); - $lesson = $course->get_lessons()[0]; - $lid = $lesson->get( 'id' ); - $qid = $lesson->get( 'quiz' ); - - $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid ); - $attempt->save(); - return $attempt; - - } - - /** - * Get a series of initialized mock sibling attempts - * - * @since 4.2.0 - * - * @param integer $num Optional. Number of sibling attmpts. Default 5. - * @param integer $num_questions Optional. Number of questions to add to the quiz. Default 5. - * @param integer $uid Optional. WordPress user id, if not passed a new user will be created. Default `null`. - * @param int[] $course Optional. Course id, if not passed a new mock course will be created. Default `null`. - * @return obj[] - */ - private function get_mock_sibling_attempts( $num = 5, $num_questions = 5, $uid = null, $course = null ) { - - $uid = $uid ? $uid : $this->factory->user->create(); - $course = ! empty( $course ) ? $course : $this->generate_mock_courses( 1, 1, 1, 1, $num_questions )[0]; - - // Create attempts. - $attempts = array(); - for ( $i = 0; $i < $num; $i++ ) { - $attempts[] = $this->get_mock_attempt( $num_questions, $uid, $course ); - } - - return $attempts; - } - - /** - * Retrieve the first incorrect choice for a given question. - * - * @since 4.0.0 - * - * @param LLMS_Question|WP_Post|int $question Question object, WP_Post object for a question post, or WP_Post ID of the question. - * @return LLMS_Question_Choice - */ - private function get_incorrect_choice( $question ) { - - $question = is_a( $question, 'LLMS_Question' ) ? $question : llms_get_post( $question ); - - foreach ( $question->get_choices() as $choice ) { - - if ( $choice->is_correct() ) { - continue; - } - - return array( - $choice->get( 'id' ), - ); - - } - - } - - /** - * [take_a_quiz description] - * @param [type] $desired_grade grade for the attempt - * @param [type] $passing_percent required passing percentage - * @param integer $num_questions number of questions in the quiz - * @param string $rand whether to randomize question order - * @param string $passing_required whether passing grade is required to complete the associated lesson - * @return [type] [description] - * @since 3.9.2 - * @version 3.17.1 - */ - private function take_a_quiz( $desired_grade, $passing_percent, $num_questions = 15, $attempt = null, $rand = 'no', $passing_required = 'no' ) { - - if ( ! $attempt ) { - $attempt = $this->get_mock_attempt( $num_questions ); - } - - update_post_meta( $attempt->get( 'lesson_id' ), '_llms_require_passing_grade', $passing_required ); - - update_post_meta( $attempt->get( 'quiz_id' ), '_llms_random_questions', $rand ); - update_post_meta( $attempt->get( 'quiz_id' ), '_llms_passing_percent', $passing_percent ); - $to_answer_correctly = 0 === $desired_grade ? 0 : $desired_grade / 100 * $num_questions; - - $attempt->start(); - - $current_question = 1; - while ( $attempt->get_next_question() ) { - - $question_id = $attempt->get_next_question(); - $question = llms_get_post( $question_id ); - - $answer_type = ( $current_question <= $to_answer_correctly ); - - // Answer correctly until we don't have to anymore. - foreach( $question->get_choices() as $key => $choice ) { - if ( $answer_type === $choice->is_correct() ) { - $attempt->answer_question( $question_id, array( $choice->get( 'id' ) ) ); - break; - } - } - - $current_question++; - - } - - $attempt->end(); - - return $attempt; - - } - - public function test_answer_question_correctly() { - - $attempt = $this->get_mock_attempt(); - $questions = wp_list_pluck( $attempt->get_questions(), 'id' ); - $question = llms_get_post( $questions[0] ); - $correct = $question->get_correct_choice(); - - // Answer question. - $attempt = $attempt->answer_question( $questions[0], $correct ); - - $this->assertTrue( is_a( $attempt, 'LLMS_Quiz_Attempt' ) ); - - $res = $attempt->get_questions()[0]; - - $this->assertEquals( $res['points'], $res['earned'] ); - $this->assertEquals( 'yes', $res['correct'] ); - $this->assertEquals( $correct, $res['answer'] ); - - - /** - * Answer the question again to simulate a user going back to change their answer. - * - * @see https://github.com/gocodebox/lifterlms/issues/1211 - */ - $incorrect = $this->get_incorrect_choice( $question ); - $attempt->answer_question( $questions[0], $incorrect ); - - $res = $attempt->get_questions()[0]; - - $this->assertEquals( 0, $res['earned'] ); - $this->assertEquals( 'no', $res['correct'] ); - $this->assertEquals( $incorrect, $res['answer'] ); - - } - - /** - * Test answer_question() when supplying a correct answer - * - * @since 4.0.0 - * - * @return void - */ - public function test_answer_question_incorrectly() { - - $attempt = $this->get_mock_attempt(); - $questions = wp_list_pluck( $attempt->get_questions(), 'id' ); - $question = llms_get_post( $questions[0] ); - $correct = $question->get_correct_choice(); - - // Answer question. - $incorrect = $this->get_incorrect_choice( $question ); - $attempt = $attempt->answer_question( $questions[0], $incorrect ); - - $res = $attempt->get_questions()[0]; - - $this->assertEquals( 0, $res['earned'] ); - $this->assertEquals( 'no', $res['correct'] ); - $this->assertEquals( $incorrect, $res['answer'] ); - - /** - * Answer the question again to simulate a user going back to change their answer. - * - * @see https://github.com/gocodebox/lifterlms/issues/1211 - */ - $attempt->answer_question( $questions[0], $correct ); - - $this->assertTrue( is_a( $attempt, 'LLMS_Quiz_Attempt' ) ); - - $res = $attempt->get_questions()[0]; - - $this->assertEquals( $res['points'], $res['earned'] ); - $this->assertEquals( 'yes', $res['correct'] ); - $this->assertEquals( $correct, $res['answer'] ); - - } - - /** - * Test answer_question() when supplying an incorrect answer - * - * @since 4.0.0 - * - * @return void - */ - public function test_grading_with_floats() { - - $attempt = $this->get_mock_attempt( 6 ); - - $questions = $attempt->get_questions(); - - foreach ( $questions as $key => &$data ) { - $data['points'] = 3.3333; - } - - $attempt->set_questions( $questions, true ); - - $attempt = $this->take_a_quiz( 67, 65, 6, $attempt ); - $this->assertEquals( 66.67, $attempt->get( 'grade' ) ); - - } - - - /** - * Test counter functions - * @return void - * @since 3.9.2 - * @version 3.16.11 - */ - public function test_get_count() { - - $i = 1; - while ( $i <= 10 ) { - - $attempt = $this->get_mock_attempt( $i ); - - // num of questions and num available points will both be the same given the default mock quiz data - foreach ( array( 'available_points', 'questions' ) as $key ) { - $this->assertEquals( $i, $attempt->get_count( $key ) ); - } - - // update each question to have a random number of points and ensure the available points from getter is correct - $questions = $attempt->get_questions(); - $total_points = 0; - foreach( $questions as $key => $question ) { - $add = rand( 1, 100 ); - $questions[ $key ]['points'] = $add; - $total_points += $add; - } - - $attempt->set_questions( $questions, true ); - - $this->assertEquals( $total_points, $attempt->get_count( 'available_points' ) ); - - $i++; - - } - - - } - - - /** - * test get student function - * @return void - * @since 3.9.0 - * @version 3.9.0 - */ - public function test_get_student() { - - $uid = $this->factory->user->create(); - $courses = $this->generate_mock_courses( 1, 1, 1, 1 ); - - $course = llms_get_post( $courses[0] ); - $lesson = $course->get_lessons()[0]; - $lid = $lesson->get( 'id' ); - $qid = $lesson->get( 'quiz' ); - - $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid ); - - $this->assertTrue( is_a( $attempt->get_student(), 'LLMS_Student' ) ); - $this->assertEquals( $uid, $attempt->get_student()->get_id() ); - - } - - // /** - // * test getters and setters and save method - // * @return void - // * @since 3.9.0 - // * @version 3.9.2 - // */ - // public function test_getters_setters_and_save() { - - // $uid = $this->factory->user->create(); - // $courses = $this->generate_mock_courses( 1, 1, 1, 1 ); - - // $course = llms_get_post( $courses[0] ); - // $lesson = $course->get_lessons()[0]; - // $lid = $lesson->get( 'id' ); - // $qid = $lesson->get( 'quiz' ); - - // $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid ); - - // $data = array( - // 'attempt' => 5, - // 'current' => false, - // 'end_date' => current_time( 'mysql' ), - // 'grade' => 85.35, - // 'passed' => true, - // 'start_date' => current_time( 'mysql' ), - // ); - - // foreach ( $data as $key => $val ) { - - // $attempt->set( $key, $val ); - // $this->assertEquals( $data[ $key ], $attempt->get( $key ) ); - - // } - - // foreach ( $attempt->get( 'questions' ) as $key => $question ) { - - // $this->assertEquals( $key + 1, $attempt->get_question_order( $question['id'] ) ); - - // } - - // // save the attempt again and ensure persistence works - // $attempt->save(); - - // $student = llms_get_student( $uid ); - // $attempt = $student->quizzes()->get_attempt( $qid, $lid, $data['attempt'] ); - // foreach ( $data as $key => $val ) { - // $this->assertEquals( $data[ $key ], $attempt->get( $key ) ); - // } - - // } - - // /** - // * test static init function - // * @return void - // * @since 3.9.0 - // * @version 3.9.0 - // */ - // public function test_init() { - - // $uid = $this->factory->user->create(); - // $courses = $this->generate_mock_courses( 1, 1, 1, 1 ); - - // $course = llms_get_post( $courses[0] ); - // $lesson = $course->get_lessons()[0]; - // $lid = $lesson->get( 'id' ); - // $qid = $lesson->get( 'assigned_quiz' ); - - // $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid )->save(); - - // $att_num = $attempt->get( 'attempt' ); - // $student = llms_get_student( $uid ); - - // // attempt saved successfully - // $this->assertEquals( $student->quizzes()->get_attempt( $qid, $lid, $att_num ), $attempt ); - - // // no user, attempt throws exception - // try { - // $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save(); - // } catch ( Exception $exception ) { - // $this->assertTrue( is_a( $exception, 'Exception' ) ); - // } - - // // no user but a current user exists - // wp_set_current_user( $uid ); - // $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save(); - // $att_num = $attempt->get( 'attempt' ); - // $this->assertEquals( 1, $att_num ); // should not increment because the attempt already exists - // $this->assertEquals( $student->quizzes()->get_attempt( $qid, $lid, $att_num ), $attempt ); - - // // mark the new attempt as not-current - // $attempt->set( 'current', false )->save(); - // $attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save(); - // // new attempt should be #2 - // $this->assertEquals( 2, $attempt->get( 'attempt' ) ); - - // } - - // /** - // * test quiz start - // * @return void - // * @since 3.9.0 - // * @version 3.9.0 - // */ - // public function test_start() { - - // $attempt = $this->get_mock_attempt( 2 ); - // $attempt->start(); - - // $this->assertTrue( ! empty( $attempt->get( 'start_date' ) ) ); - - // } - - /** - * Take a bunch of quizzes - * quiz taking / ending functions - * Tests grade / point calculations - * pass/fail/complete actions - * @return void - * @since 3.9.2 - * @version 3.17.4 - */ - public function test_take_some_quizzes( ) { - - $i = 0; - $num_tests = 0; - $num_pass = 0; - $num_fail = 0; - while ( $i <= 100 ) { - - $rand = rand( 0, 1 ) ? 'yes' : 'no'; - $passing = $rand = rand( 0, 1 ) ? 'yes' : 'no'; - $attempt = $this->take_a_quiz( $i, 65, 25, null, $rand, $passing ); - - if ( 0 === $i ) { - $grade = 0; - } else { - $weight = ( 100 / $attempt->get_count( 'available_points' ) ); - $grade = floor( $i / 100 * 25 ) * $weight; - } - - $this->assertEquals( $grade, $attempt->get( 'grade' ) ); - $this->assertTrue( ! is_null( $attempt->get( 'end_date' ) ) ); - - if ( $grade < 65 ) { - $num_fail++; - $this->assertFalse( $attempt->is_passing() ); - $is_complete = llms_parse_bool( $passing ) ? false : true; - } else { - $num_pass++; - $this->assertTrue( $attempt->is_passing() ); - $is_complete = true; - } - - $this->assertEquals( $is_complete, llms_is_complete( $attempt->get( 'student_id' ), $attempt->get( 'lesson_id' ), 'lesson' ) ); - - $num_tests++; - - $i = $i + ( 5 * rand( 1, 20 ) ); - - } - - } - - /** - * Test get siblings - * - * @return void - */ - public function test_get_siblings() { - - $attempts = $this->get_mock_sibling_attempts( 5, 1 ); - $attempt_ids = array_map( - function( $attempt ) { - return $attempt->get( 'id' ); - }, - $attempts - ); - - - // Test get siblings of the first attempt equals to the created array of attempts (id). - $this->assertEquals( - array_reverse( $attempt_ids ), - $attempts[0]->get_siblings( array(), 'ids' ) - ); - - // Test exclude. - $this->assertEquals( - array_reverse( array_slice( $attempt_ids, 1, count( $attempt_ids ) ) ), - $attempts[0]->get_siblings( - array( - 'exclude' => array( $attempt_ids[0] ), - ), - 'ids' - ) - ); - - // Test per page, get only 4 attempts out of 5. - $this->assertEquals( - array_slice( array_reverse( $attempt_ids ), 0, count( $attempt_ids ) - 1 ), - $attempts[0]->get_siblings( - array( - 'per_page' => count( $attempt_ids ) - 1, - ), - 'ids' - ) - ); - - // Test return as attempt. - $is_attempt = $attempts[0]->get_siblings( - array( - 'per_page' => 1, - ), - 'attempts' - )[0] instanceof LLMS_Quiz_Attempt; - $is_attempt_two = $attempts[0]->get_siblings( - array( - 'per_page' => 1, - ), - 'whatever' - )[0] instanceof LLMS_Quiz_Attempt; - $this->assertTrue( $is_attempt ); - - } - - /** - * Test lesson completion on delete attempts for a lesson not requiring a passing grade - * - * @return void - */ - public function test_delete_not_requiring_passing_grade_lesson() { - - // Create 3 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take a quiz (no passing). This will mark the lesson as complete. - $this->take_a_quiz( 0, 65, 1, $attempts[0] ); - - // Only the last deletion will mark the lesson as incomplete. - foreach ( $attempts as $attempt ) { - $this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - $attempt->delete(); - } - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - // Create 3 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take a quiz (passing). This will mark the lesson as complete. - $this->take_a_quiz( 100, 65, 1, $attempts[0] ); - - // We have 1 passing attempt, still only the last deletion will mark the lesson as incomplete. - foreach ( $attempts as $attempt ) { - $this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - $attempt->delete(); - } - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - } - - /** - * Test lesson completion on delete attempts for a lesson requiring a passing grade - * - * @return void - */ - public function test_delete_requiring_passing_grade_lesson() { - - // Create 3 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take a quiz (no passing). This will NOT mark the lesson as complete. - $this->take_a_quiz( 0, 65, 1, $attempts[0], 'no', 'yes' ); - - foreach ( $attempts as $attempt ) { - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - $attempt->delete(); - } - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - // Create 3 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take a quiz (passing). This will mark the lesson as complete. - $this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' ); - - // We have 1 passing attempt (the first), deleting all the others (see the reverse order) will not mark the lesson as incomplete. - foreach ( array_reverse( $attempts ) as $attempt ) { - $this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - $attempt->delete(); - } - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - // Create 1 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take a quiz (passing). This will mark the lesson as complete. - $this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' ); - - // We have 1 passing attempt (the first), deleting it will mark the lesson as incomplete. - $attempts[0]->delete(); - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - // Create 3 attempts (for a quiz with 1 question), for a given lesson. - $attempts = $this->get_mock_sibling_attempts( 3, 1 ); - $lesson_id = $attempts[0]->get( 'lesson_id' ); - $student_id = $attempts[0]->get( 'student_id' ); - - // Take two passing quizzes. - $this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' ); - //$this->take_a_quiz( 100, 65, 1, $attempts[1], 'no', 'yes' ); - - // We have 2 passing attempts (the first two), the lesson will be marked as incomplete only after deleting all the passed attempts. - foreach ( array_reverse( $attempts ) as $attempt ) { - $this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - $attempt->delete(); - } - $this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) ); - - } - - /** - * Test get_question_objects() method when filtering out the removed questions. - * - * @since 5.3.0 - * - * @return void - */ - public function test_get_question_objects_filter_removed() { - - $attempt = $this->get_mock_attempt(); - $questions = wp_list_pluck( $attempt->get_questions(), 'id' ); - - // Check `get_question_objects()` returns the same list of `get_questions()`. - $this->assertEqualSets( - $questions, - $this->question_object_ids_list_pluck( $attempt ) - ); - - // Delete a question. - wp_delete_post( $questions[ 1 ] ); - - // Check `get_question_objects()` still returns the same list of `get_questions()`. - $this->assertEqualSets( - $questions, - $this->question_object_ids_list_pluck( $attempt ) - ); - - // Check `get_question_objects()` returns the same list of `get_questions()` except for the removed question - // when the `$filter_remove` is passed as true. - $this->assertEqualSets( - array_merge( - array( - $questions[0] - ), - array_slice( - $questions, - 2 - ) - ), - $this->question_object_ids_list_pluck( $attempt, true, true ) - ); - - } - - /** - * Returns a question object id given a LLMS_Quiz_Attempt - * - * @since 5.3.0 - * - * @param LLMS_Quiz_Attempt $attemt Attempt object. - * @return void - */ - private function question_object_ids_list_pluck( $attempt, $cache = true, $filter_removed = false ) { - return array_filter( - array_map( - function( $qo ) { - return $qo->get('id'); - }, - $attempt->get_question_objects( $cache, $filter_removed ) - ) - ); - } -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz.php deleted file mode 100644 index 7d888d1fa9..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz.php +++ /dev/null @@ -1,461 +0,0 @@ -<?php -/** - * Tests for LifterLMS Quiz Model - * - * @package LifterLMS_Tests/Models - * - * @group post_models - * @group quizzes - * @group quiz - * - * @since 3.16.0 - * @since 3.37.2 Added test coverage for many untested methods. - * @since 4.2.0 Added test coverage for `is_orphan()` method with `$deep` param set to true. - */ -class LLMS_Test_LLMS_Quiz extends LLMS_PostModelUnitTestCase { - - /** - * class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_Quiz'; - - /** - * db post type of the model being tested - * - * @var string - */ - protected $post_type = 'llms_quiz'; - - /** - * Get properties, used by test_getters_setters - * - * This should match, exactly, the object's $properties array. - * - * @since 3.16.0 - * - * @return string[] - */ - protected function get_properties() { - return array( - 'lesson_id' => 'absint', - ); - } - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters. - * - * @since 3.16.0 - * - * @return array - */ - protected function get_data() { - return array( - 'lesson_id' => 123, - ); - } - - - /** - * Test the questions()->create_question() method. - * - * @since 3.16.0 - * - * @return void - */ - public function test_create_question() { - - $this->create( 'test title' ); - $this->assertTrue( is_numeric( $this->obj->questions()->create_question() ) ); - - } - - /** - * Test the questions()->delete_question() method. - * - * @since 3.16.0 - * - * @return void - */ - public function test_delete_question() { - - $this->create( 'test title' ); - $qid = $this->obj->questions()->create_question(); - $this->assertTrue( $this->obj->questions()->delete_question( $qid ) ); - - // belongs to another quiz, can't delete - $this->create( 'second question' ); - $this->assertFalse( $this->obj->questions()->delete_question( $qid ) ); - - // doesn't exist - $this->assertFalse( $this->obj->questions()->delete_question( 999999999 ) ); - - } - - /** - * Test get_course() on a quiz with no parent lesson. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_course_no_lesson() { - - $this->create( 'test title' ); - $this->assertFalse( $this->obj->get_course() ); - - } - - /** - * Test get_course() on a quiz with a parent lesson which has no parent course. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_course_lesson_no_course() { - - $this->create( 'test title' ); - $lesson = llms_get_post( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) ); - $lesson->set( 'quiz', $this->obj->get( 'id' ) ); - - $this->assertFalse( $this->obj->get_course() ); - - } - - /** - * Test get_course() success. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_course() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) ); - $lesson = $course->get_lessons()[0]; - $quiz = $lesson->get_quiz(); - $this->assertEquals( $course, $quiz->get_course() ); - - } - - /** - * Test get_lesson() when no value is set. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_lesson_no_value() { - - $this->create( 'test title' ); - $this->obj->set( 'lesson_id', '' ); - $this->assertFalse( $this->obj->get_lesson() ); - - } - - /** - * Test get_lesson() when the value is an invalid post. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_lesson_invalid() { - - $this->create( 'test title' ); - $post = $this->factory->post->create(); - $this->obj->set( 'lesson_id', ++$post ); - $this->assertNull( $this->obj->get_lesson() ); - - } - - /** - * Test get_lesson() success. - * - * @since 3.37.2 - * - * @return void - */ - public function test_get_lesson() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) ); - $lesson = $course->get_lessons()[0]; - $quiz = $lesson->get_quiz(); - $this->assertEquals( $lesson, $quiz->get_lesson() ); - - } - - /** - * Test the questions()->get_question() method. - * - * @since 3.16.0 - * - * @return void - */ - public function test_get_question() { - - $this->create( 'test title' ); - $qid = $this->obj->questions()->create_question(); - $this->assertTrue( is_a( $this->obj->questions()->get_question( $qid ), 'LLMS_Question' ) ); - - // question doesn't belong to quiz so it should return false - $this->create( 'second question' ); - $this->assertFalse( $this->obj->questions()->get_question( $qid ) ); - - // question doesn't exist - $this->assertFalse( $this->obj->questions()->get_question( 9999999 ) ); - - } - - /** - * Test the questions() method. - * - * @since 3.16.0 - * - * @return void - */ - public function test_get_questions() { - - $this->create( 'test title' ); - $i = 1; - while( $i <= 3 ) { - $this->obj->questions()->create_question(); - $i++; - } - - // check default 'questions' - $questions = $this->obj->get_questions(); - $this->assertEquals( 3, count( $questions ) ); - foreach ( $questions as $question ) { - $this->assertInstanceOf( 'LLMS_Question', $question ); - } - - // check posts return - $questions = $this->obj->get_questions( 'posts' ); - $this->assertEquals( 3, count( $questions ) ); - foreach ( $questions as $question ) { - $this->assertInstanceOf( 'WP_Post', $question ); - } - - // check id return - $questions = $this->obj->get_questions( 'ids' ); - $this->assertEquals( 3, count( $questions ) ); - foreach ( $questions as $question ) { - $this->assertTrue( is_numeric( $question ) ); - } - - } - - /** - * Test the has_attempt_limit() method. - * - * @since 3.37.2 - * - * @return void - */ - public function test_has_attempt_limit() { - - $this->create(); - - // No value set. - $this->obj->set( 'limit_attempts', '' ); - $this->assertFalse( $this->obj->has_attempt_limit() ); - - // Explicit no. - $this->obj->set( 'limit_attempts', 'no' ); - $this->assertFalse( $this->obj->has_attempt_limit() ); - - // Something unexpected (still no). - $this->obj->set( 'limit_attempts', 'fake' ); - $this->assertFalse( $this->obj->has_attempt_limit() ); - - // Yes.. - $this->obj->set( 'limit_attempts', 'yes' ); - $this->assertTrue( $this->obj->has_attempt_limit() ); - - } - - /** - * Test the has_time_limit() method. - * - * @since 3.37.2 - * - * @return void - */ - public function test_has_time_limit() { - - $this->create(); - - // No value set. - $this->obj->set( 'limit_time', '' ); - $this->assertFalse( $this->obj->has_time_limit() ); - - // Explicit no. - $this->obj->set( 'limit_time', 'no' ); - $this->assertFalse( $this->obj->has_time_limit() ); - - // Something unexpected (still no). - $this->obj->set( 'limit_time', 'fake' ); - $this->assertFalse( $this->obj->has_time_limit() ); - - // Yes.. - $this->obj->set( 'limit_time', 'yes' ); - $this->assertTrue( $this->obj->has_time_limit() ); - - } - - /** - * Test is_open() with no student. - * - * @since 3.37.2 - * - * @return void - */ - public function test_is_open_no_student() { - - $this->create(); - $this->assertFalse( $this->obj->is_open() ); - - } - - /** - * Test is_open() with a student when there's no attempt limits. - * - * @since 3.37.2 - * - * @return void - */ - public function test_is_open_with_student_no_limits() { - - $this->create(); - - $user = $this->factory->student->create(); - - // Pass in a user id. - $this->assertTrue( $this->obj->is_open( $user ) ); - - // Use the current session's user. - wp_set_current_user( $user ); - $this->assertTrue( $this->obj->is_open() ); - - } - - /** - * Test is_open() with a student when there are attempt limits. - * - * @since 3.37.2 - * - * @return void - */ - public function test_is_open_with_student_with_limits() { - - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) ); - $lesson = $course->get_lessons()[0]; - $quiz = $lesson->get_quiz(); - - $quiz->set( 'limit_attempts', 'yes' ); - $quiz->set( 'allowed_attempts', 1 ); - - $user = $this->factory->student->create(); - - // Use the current session's user. - wp_set_current_user( $user ); - $this->assertTrue( $quiz->is_open() ); - - // Pass in a user id. - $this->assertTrue( $quiz->is_open( $user ) ); - - // Take the quiz. - $this->take_quiz( $quiz->get( 'id' ), $user ); - - // Use the current session's user. - $this->assertFalse( $quiz->is_open() ); - - // Pass in a user id. - $this->assertFalse( $quiz->is_open( $user ) ); - - } - - /** - * Test the is_orphan() method. - * - * @since 3.37.2 - * - * @return void - */ - public function test_is_orphan() { - - $this->create(); - - $this->obj->set( 'lesson_id', '' ); - $this->assertTrue( $this->obj->is_orphan() ); - - $this->obj->set( 'lesson_id', 123 ); - $this->assertFalse( $this->obj->is_orphan() ); - - } - - /** - * Test the is_orphan() method with the $deep param set to true - * - * @since 4.2.0 - * - * @return void - */ - public function test_is_orphan_deep() { - - $this->create(); - $lesson = llms_get_post( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) ); - $lesson->set( 'quiz', $this->obj->get( 'id' ) ); - - // Quiz `lesson_id` meta unset, we expect both `is_orpan()` and `is_orphan( $deep = true )` to return true. - $this->obj->set( 'lesson_id', '' ); - $this->assertTrue( $this->obj->is_orphan() ); - $this->assertTrue( $this->obj->is_orphan( true ) ); - - // Quiz `lesson_id` set as `$lesson`'s id, we expect both `is_orpan()` and `is_orphan( $deep = true )` to return false. - $this->obj->set( 'lesson_id', $lesson->get( 'id' ) ); - $this->assertFalse( $this->obj->is_orphan() ); - $this->assertFalse( $this->obj->is_orphan( true ) ); - - // 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. - $lesson->set( 'quiz', 123 ); - $this->assertFalse( $this->obj->is_orphan() ); - $this->assertTrue( $this->obj->is_orphan( true ) ); - - // 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. - $this->obj->set( 'lesson_id', 123 ); - $this->assertFalse( $this->obj->is_orphan() ); - $this->assertTrue( $this->obj->is_orphan( true ) ); - - } - - /** - * Test the questions()->update_question() method. - * - * @since 3.16.0 - * - * @return void - */ - public function test_update_question() { - - $this->create( 'test title' ); - - // create when no id supplied - $id = $this->obj->questions()->update_question(); - $this->assertTrue( is_numeric( $id ) ); - - // update should return it's own id - $this->assertEquals( $id, $this->obj->questions()->update_question( array( 'id' => $id ) ) ); - - // can't update from another quiz - $this->create( 'second question' ); - $this->assertFalse( $this->obj->questions()->update_question( array( 'id' => $id ) ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-section.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-section.php deleted file mode 100644 index 11fa7dad8c..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-section.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php -/** - * Tests for LifterLMS Course Model - * @group LLMS_Section - * @group LLMS_Post_Model - * @since 3.24.0 - * @version 3.24.0 - */ -class LLMS_Test_LLMS_Section extends LLMS_PostModelUnitTestCase { - - /** - * class name for the model being tested by the class - * @var string - */ - protected $class_name = 'LLMS_Section'; - - /** - * db post type of the model being tested - * @var string - */ - protected $post_type = 'section'; - - /** - * Get properties, used by test_getters_setters - * This should match, exactly, the object's $properties array - * @return array - * @since 3.24.0 - * @version 3.24.0 - */ - protected function get_properties() { - return array( - 'order' => 'absint', - 'parent_course' => 'absint', - ); - } - - /** - * Get data to fill a create post with - * This is used by test_getters_setters - * @return array - * @since 3.24.0 - * @version 3.24.0 - */ - protected function get_data() { - return array( - 'order' => 1, - 'parent_course' => 12345, - ); - } - - /** - * the the count_elements() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_count_elements() { - - $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - $this->assertEquals( 0, $section->count_elements() ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 0 )[0] ); - $section = llms_get_post( $course->get_sections( 'ids' )[0] ); - $this->assertEquals( 5, $section->count_elements() ); - - } - - /** - * the the get_course() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_course() { - - $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - $this->assertNull( $section->get_course() ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 0 )[0] ); - $section = llms_get_post( $course->get_sections( 'ids' )[0] ); - $this->assertTrue( is_a( $section->get_course(), 'LLMS_Course' ) ); - - } - - // public function test_get_next() { - - // $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - // $this->assertNull( $section->get_course() ); - // var_dump( $section->get_next() ); - - // } - - /** - * the the get_percent_complete() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_percent_complete() { - - $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - $this->assertEquals( 0, $section->get_percent_complete() ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 4, 4, 0, 0 )[0] ); - $student = $this->get_mock_student(); - $student->enroll( $course->get( 'id' ) ); - $uid = $student->get( 'id' ); - - foreach ( $course->get_sections() as $i => $section ) { - - $perc = ( $i + 1 ) * 25; - - // get student by ID - $this->assertEquals( 0, $section->get_percent_complete( $uid ) ); - - // get from current user - $this->assertEquals( 0, $section->get_percent_complete() ); - - // complete 50% of the lessons in the section - $this->complete_courses_for_student( $uid, $course->get( 'id' ), ( $perc / 2 ) + ( $i * 12.5 ) ); - $this->assertEquals( 50, $section->get_percent_complete( $uid ) ); - - // complete the entire section - $this->complete_courses_for_student( $uid, $course->get( 'id' ), $perc ); - $this->assertEquals( 100, $section->get_percent_complete( $uid ) ); - - // check as the current user - wp_set_current_user( $uid ); - $this->assertEquals( 100, $section->get_percent_complete() ); - wp_set_current_user( null ); // reset - - } - - } - - // public function test_get_previous() {} - - /** - * the the get_lessons() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_lessons() { - - $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - $lessons = $section->get_lessons( 'ids' ); - $this->assertEquals( 0, count( $lessons ) ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 4, 0, 0 )[0] ); - $section = llms_get_post( $course->get_sections( 'ids' )[0] ); - - // get just ids - $lessons = $section->get_lessons( 'ids' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $id ) { - $this->assertTrue( is_numeric( $id ) ); - }, $lessons ); - - // wp post objects - $lessons = $section->get_lessons( 'posts' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $post ) { - $this->assertTrue( is_a( $post, 'WP_Post' ) ); - }, $lessons ); - - // lesson objects - $lessons = $section->get_lessons( 'sections' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $lesson ) { - $this->assertTrue( is_a( $lesson, 'LLMS_Lesson' ) ); - }, $lessons ); - - } - - /** - * the the get_children_lessons() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_children_lessons() { - - $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) ); - $lessons = $section->get_lessons( 'ids' ); - $this->assertEquals( 0, count( $lessons ) ); - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 4, 0, 0 )[0] ); - $section = llms_get_post( $course->get_sections( 'ids' )[0] ); - - // wp post objects - $lessons = $section->get_lessons( 'posts' ); - $this->assertEquals( 4, count( $lessons ) ); - array_map( function( $post ) { - $this->assertTrue( is_a( $post, 'WP_Post' ) ); - }, $lessons ); - - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-student.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-student.php deleted file mode 100644 index fd584b695e..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-student.php +++ /dev/null @@ -1,170 +0,0 @@ -<?php -/** - * Tests for LifterLMS Student Model - * @group LLMS_Student - * @group LLMS_Student_Model - * - * @since 3.33.0 - * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion. - * - * @version [vrsion] - */ -class LLMS_Test_LLMS_Student extends LLMS_UnitTestCase { - - /** - * Setup test - * - * @since 3.33.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->student = $this->get_mock_student(); - // Create new course - $this->course_id = $this->factory->post->create( array( - 'post_type' => 'course', - )); - // Create new membership - $this->memb_id = $this->factory->post->create( array( - 'post_type' => 'llms_membership', - )); - - } - - /** - * Functional test for the enroll() method. - * - * @since 3.33.0 - * @see user/class-llms-test-student.php for integration tests. - * - * @return void - */ - public function test_enroll() { - - // check against both courses and memberships - - // enroll in a non existent course/membership - $this->assertFalse( $this->student->enroll( $this->course_id + 100, 'test_is_enrolled' ) ); - $this->assertEquals( 0, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) ); - $this->assertFalse( $this->student->enroll( $this->memb_id + 100, 'test_is_enrolled' ) ); - $this->assertEquals( 0, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) ); - - // enroll a student - $this->assertTrue( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) ); - $this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) ); - $this->assertTrue( $this->student->enroll( $this->memb_id, 'test_is_enrolled' ) ); - $this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) ); - - // enroll a student twice - $this->assertFalse( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) ); - $this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) ); - - // check re-enroll - $this->student->unenroll( $this->course_id, 'test_is_enrolled', 'expired' ); - $this->assertTrue( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) ); - $this->assertEquals( 2, did_action( 'llms_user_enrolled_in_course' ) ); - $this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) ); - - } - - /** - * Functional test for the unenroll() method. - * - * @since 3.33.0 - * @see user/class-llms-test-student.php for integration tests. - * - * @return void - */ - public function test_unenroll() { - - // unenroll a non enrolled student - $this->assertFalse( $this->student->unenroll( $this->course_id ) ); - $this->assertEquals( 0, did_action( 'llms_user_removed_from_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_removed_from_membership_level' ) ); - $this->assertFalse( $this->student->unenroll( $this->memb_id ) ); - $this->assertEquals( 0, did_action( 'llms_user_removed_from_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_removed_from_membership_level' ) ); - - // unenroll a student in a course - $this->student->enroll( $this->course_id ); - $this->assertTrue( $this->student->unenroll( $this->course_id ) ); - $this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) ); - $this->assertEquals( 0, did_action( 'llms_user_removed_from_membership_level' ) ); - - // unenroll a student in a membership - $this->student->enroll( $this->memb_id ); - $this->assertTrue( $this->student->unenroll( $this->memb_id ) ); - $this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) ); - $this->assertEquals( 1, did_action( 'llms_user_removed_from_membership_level' ) ); - - // try to unenroll a student with a different trigger - $this->student->enroll( $this->memb_id ); - $res = $this->student->unenroll( $this->memb_id, $this->student->get_enrollment_trigger( $this->memb_id ) . '_test' ); - $this->assertFalse( $res ); - $this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) ); - $this->assertEquals( 1, did_action( 'llms_user_removed_from_membership_level' ) ); - - } - - /** - * Functional test for the delete_enrollment() method. - * - * @since 3.33.0 - * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion. - * @see user/class-llms-test-student.php for integration tests. - * - * @return void - */ - public function test_delete_enrollment() { - - // delete a non existent enrollment: user not enrolled at all. - $this->assertFalse( $this->student->delete_enrollment( $this->course_id ) ); - $this->assertEquals( 0, did_action( 'llms_user_enrollment_deleted' ) ); - - // enroll a student. - $this->student->enroll( $this->course_id ); - - // delete a non existent enrollment: user enrolled with a different trigger. - $res = $this->student->delete_enrollment( $this->course_id, $this->student->get_enrollment_trigger( $this->course_id ) . '_test' ); - $this->assertFalse( $res ); - $this->assertEquals( 0, did_action( 'llms_user_enrollment_deleted' ) ); - - // delete an existent enrollment. - $this->assertTrue( $this->student->delete_enrollment( $this->course_id , $this->student->get_enrollment_trigger( $this->course_id ) ) ); - $this->assertEquals( 1, did_action( 'llms_user_enrollment_deleted' ) ); - - $this->student->enroll( $this->course_id ); - - // delete an existent enrollment: any trigger. - $this->assertTrue( $this->student->delete_enrollment( $this->course_id ) ); - $this->assertEquals( 2, did_action( 'llms_user_enrollment_deleted' ) ); - - // Test auto-enrollments deletion. - - // create a membership. - $membership = new LLMS_Membership( 'new', 'Membership Title' ); - $membership_id = $membership->get('id'); - // create two courses and set them as membership auto-enrollments. - $courses = $this->factory->course->create_many( 2, array( 0, 0, 0, 0 ) ); - $membership->set( 'auto_enroll', $courses ); - - $actions = did_action( 'llms_user_enrollment_deleted' ); - - // enroll a student to the membership. - $this->student->enroll( $membership_id ); - - $res = $this->student->delete_enrollment( $membership_id, $this->student->get_enrollment_trigger( $membership_id ) ); - $this->assertTrue( $res ); - // test we had 3 deletion: the membership, and the related courses. - $this->assertEquals( $actions + 3, did_action( 'llms_user_enrollment_deleted' ) ); - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-transaction.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-transaction.php deleted file mode 100644 index e350cd8a88..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-transaction.php +++ /dev/null @@ -1,93 +0,0 @@ -<?php -/** - * Tests for LifterLMS Transaction Model - * - * @group LLMS_Transaction - * @group LLMS_Post_Model - * - * @since 5.9.0 - */ -class LLMS_Test_LLMS_Transaction extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class. - * - * @var string - */ - protected $class_name = 'LLMS_Transaction'; - - /** - * DB post type of the model being tested. - * - * @var string - */ - protected $post_type = 'llms_transaction'; - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters - * - * @since 5.9.0 - * - * @return array - */ - protected function get_data() { - return array( - 'api_mode' => 'live', - 'amount' => 25.99, - 'currency' => 'USD', - 'gateway_completed_date' => '2021-02-24 23:23:59', - 'gateway_customer_id' => 'customer_id', - 'gateway_fee_amount' => 1.99, - 'gateway_source_id' => 'source_id', - 'gateway_source_description' => 'Vist **** 2342', - 'gateway_transaction_id' => 'transaction_id', - 'order_id' => 123, - 'payment_type' => 'recurring', - 'payment_gateway' => 'payway', - 'refund_amount' => 2.99, - 'refund_data' => array( 'stuff' => 123 ), - ); - } - - /** - * Test creation of the model. - * - * @since 5.9.0 - * - * @return void - */ - public function test_create_model() { - - llms_mock_current_time( '2021-03-05 01:05:23' ); - - $this->create( 123 ); - - $id = $this->obj->get( 'id' ); - - $test = llms_get_post( $id ); - - $this->assertEquals( $id, $test->get( 'id' ) ); - $this->assertEquals( 'llms_transaction', $test->get( 'type' ) ); - $this->assertEquals( 'Transaction for Order #123 – Mar 05, 2021 @ 01:05 AM', $test->get( 'title' ) ); - - $this->assertEquals( '2021-03-05 01:05:23', $test->get( 'date' ) ); - $this->assertEquals( 'llms-txn-pending', $test->get( 'status' ) ); - - $this->assertTrue( post_password_required( $id ) ); - - } - - /** - * Skip unneeded test. - * - * @since 5.9.0 - * - * @return void - */ - public function test_edit_date() { - $this->assertTrue( true ); - } - -} diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-user-certificate.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-user-certificate.php deleted file mode 100644 index d26815e4f8..0000000000 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-user-certificate.php +++ /dev/null @@ -1,289 +0,0 @@ -<?php -/** - * Tests for earned user certificates - * - * @group models - * @group certificates - * @group LLMS_User_Certificate - * - * @since 4.5.0 - */ -class LLMS_Test_LLMS_User_Certificate extends LLMS_PostModelUnitTestCase { - - /** - * Class name for the model being tested by the class - * - * @var string - */ - protected $class_name = 'LLMS_User_Certificate'; - - /** - * DB post type of the model being tested - * - * @var string - */ - protected $post_type = 'llms_my_certificate'; - - /** - * Get data to fill a create post with - * - * This is used by test_getters_setters. - * - * @since 4.5.0 - * - * @return array - */ - protected function get_data() { - return array( - 'certificate_title' => 'Eaned Cert Title', - 'certificate_image' => 1, - 'certificate_template' => 2, - 'allow_sharing' => 'no', - ); - } - - /** - * Test creation of the model - * - * @since 4.5.0 - * - * @return void - */ - public function test_create_model() { - - $this->create( 'test title' ); - - $id = $this->obj->get( 'id' ); - - $test = new LLMS_User_Certificate( $id ); - - $this->assertEquals( $id, $test->get( 'id' ) ); - $this->assertEquals( $this->post_type, $test->get( 'type' ) ); - $this->assertEquals( 'test title', $test->get( 'title' ) ); - - } - - /** - * Test delete() method - * - * @since 4.5.0 - * - * @return void - */ - public function test_delete() { - - global $wpdb; - - $uid = $this->factory->student->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $this->factory->post->create() ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - $actions = array( - 'before' => did_action( 'llms_before_delete_certificate' ), - 'after' => did_action( 'llms_delete_certificate' ), - ); - - $cert->delete(); - - // User meta is gone. - $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}" ); - $this->assertEquals( array(), $res ); - - // Post is deleted. - $this->assertNull( get_post( $cert_id ) ); - - // Ran actions. - $this->assertEquals( ++$actions['before'], did_action( 'llms_before_delete_certificate' ) ); - $this->assertEquals( ++$actions['after'], did_action( 'llms_delete_certificate' ) ); - - } - - /** - * Test get_earned_date() - * - * @since 4.5.0 - * - * @return void - */ - public function test_get_earned_date() { - - $this->create(); - - $date = $this->obj->post->post_date; - - // Request a format. - $this->assertEquals( $date, $this->obj->get_earned_date( 'Y-m-d H:i:s' ) ); - - // Default blog format. - $this->assertEquals( date( 'F j, Y', strtotime( $date ) ), $this->obj->get_earned_date() ); - - } - - /** - * Test get_related_post_id() - * - * @since 4.5.0 - * - * @return void - */ - public function test_get_related_post_id() { - - $uid = $this->factory->student->create(); - $related = $this->factory->post->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $related ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - $this->assertEquals( $related, $cert->get_related_post_id() ); - - } - - - /** - * Test get_user_id() - * - * @since 4.5.0 - * - * @return void - */ - public function test_get_user_id() { - - $uid = $this->factory->student->create(); - $related = $this->factory->post->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $related ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - $this->assertEquals( $uid, $cert->get_user_id() ); - - } - - /** - * Test get_user_postmeta() - * - * @since 4.5.0 - * - * @return void - */ - public function test_get_user_postmeta() { - - $uid = $this->factory->student->create(); - $related = $this->factory->post->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $related ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - $expect = new stdClass(); - $expect->user_id = $uid; - $expect->post_id = $related; - $this->assertEquals( $expect, $cert->get_user_postmeta() ); - - } - - /** - * Test can_user_manage() - * - * @since 4.5.0 - * - * @return void - */ - public function test_can_user_manage() { - - $admin = $this->factory->user->create( array( 'role' => 'administrator' ) ); - $other = $this->factory->student->create(); - $uid = $this->factory->student->create(); - $related = $this->factory->post->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $related ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - // Other student cannot manage. - $this->assertFalse( $cert->can_user_manage() ); - $this->assertFalse( $cert->can_user_manage( $other ) ); - - // Fake user cannot manage. - $this->assertFalse( $cert->can_user_manage( $uid + 1 ) ); - - // Admin can. - $this->assertTrue( $cert->can_user_manage( $admin ) ); - - // Owner can. - $this->assertTrue( $cert->can_user_manage( $uid ) ); - - // Current user cannot manage. - $this->assertFalse( $cert->can_user_manage() ); - - // Current User Can. - wp_set_current_user( $admin ); - $this->assertTrue( $cert->can_user_manage() ); - - // Current user is owner. - wp_set_current_user( $uid ); - $this->assertTrue( $cert->can_user_manage() ); - - } - - /** - * Test can_user_view() - * - * @since 4.5.0 - * - * @return void - */ - public function test_can_user_view() { - - $uid = $this->factory->student->create(); - $related = $this->factory->post->create(); - $earned = $this->earn_certificate( $uid, $this->create_certificate_template(), $related ); - $cert_id = $earned[1]; - $cert = new LLMS_User_Certificate( $cert_id ); - - // Any user that can manage can always view the cert. - add_filter( 'llms_certificate_can_user_manage', '__return_true' ); - $this->assertTrue( $cert->can_user_view() ); - remove_filter( 'llms_certificate_can_user_manage', '__return_true' ); - - add_filter( 'llms_certificate_can_user_manage', '__return_false' ); - - // User cannot manage so they cannot view. - $this->assertFalse( $cert->can_user_view() ); - - // Unless sharing is enabled. - $cert->set( 'allow_sharing', 'yes' ); - $this->assertTrue( $cert->can_user_view() ); - - // Explicitly disabled. - $cert->set( 'allow_sharing', 'no' ); - $this->assertFalse( $cert->can_user_view() ); - - remove_filter( 'llms_certificate_can_user_manage', '__return_false' ); - - - } - - /** - * Test is_sharing_enabled() - * - * @since 4.5.0 - * - * @return void - */ - public function test_is_sharing_enabled() { - - $cert = new LLMS_User_Certificate( 'new', 'test' ); - - // No set. - $this->assertFalse( $cert->is_sharing_enabled() ); - - // Explicitly disabled. - $cert->set( 'allow_sharing', 'no' ); - $this->assertFalse( $cert->is_sharing_enabled() ); - - // Enabled. - $cert->set( 'allow_sharing', 'yes' ); - $this->assertTrue( $cert->is_sharing_enabled() ); - - } - -} diff --git a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-achievement-earned.php b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-achievement-earned.php deleted file mode 100644 index 6774d4e123..0000000000 --- a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-achievement-earned.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -/** - * LLMS_Notification Achievement Earned - */ - -class LLMS_Test_Notification_Achievement_Earned extends LLMS_NotificationTestCase { - - protected $notification_id = 'achievement_earned'; - -} diff --git a/tests/phpunit/unit-tests/notifications/class-llms-test-notifications.php b/tests/phpunit/unit-tests/notifications/class-llms-test-notifications.php deleted file mode 100644 index ef438a2c70..0000000000 --- a/tests/phpunit/unit-tests/notifications/class-llms-test-notifications.php +++ /dev/null @@ -1,256 +0,0 @@ -<?php -/** - * LLMS_Notifications Tests - * - * @package LifterLMS/Tests/Notifications - * - * @since 3.8.0 - * @since 3.38.0 "DRY"ed existing tests and added tests for processor scheduling related functions. - * - * @group notifications - */ -class LLMS_Test_Notifications extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 3.38.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - $this->main = LLMS()->notifications(); - - } - - /** - * Tear down the test case - * - * @since 3.38.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - - // Clear data for later tests. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'processors_to_dispatch', array() ); - - } - - /** - * Test dispatch_processor_async() for a fake processor. - * - * @since 3.38.0 - * - * @return void - */ - public function test_dispatch_processor_async_fake() { - - $res = $this->main->dispatch_processor_async( 'fake-processor' ); - $this->assertIsWPError( $res ); - $this->assertWPErrorCodeEquals( 'invalid-processor', $res ); - - } - - /** - * Test dispatch_processor_async() for a fake processor. - * - * @since 3.38.0 - * - * @return void - */ - public function test_dispatch_processor() { - - $res = $this->main->dispatch_processor_async( 'email' ); - $this->assertTrue( ! is_wp_error( $res ) ); - - } - - /** - * Test the get_controller() method - * - * @since 3.8.0 - * @since 3.38.0 Use $this->main for code DRYness. - * - * @return void - */ - public function test_get_controller() { - - // return the controller instance - $this->assertTrue( is_a( $this->main->get_controller( 'lesson_complete' ), 'LLMS_Notification_Controller_Lesson_Complete' ) ); - - // return false - $this->assertFalse( $this->main->get_controller( 'thisisveryveryfake' ) ); - - } - - /** - * Test get_controllers() method - * - * @since 3.8.0 - * @since 3.38.0 Use $this->main for code DRYness. - * - * @return void - */ - public function test_get_controllers() { - - // should always return an array - $this->assertTrue( is_array( $this->main->get_controllers() ) ); - - // each item in the array must extend the controller abstract - foreach ( $this->main->get_controllers() as $controller ) { - $this->assertTrue( is_subclass_of( $controller, 'LLMS_Abstract_Notification_Controller' ) ); - } - - } - - /** - * Test get_processor() method - * - * @since 3.8.0 - * @since 3.38.0 Use $this->main for code DRYness. - * - * @return void - */ - public function test_get_processor() { - - // return the controller instance - $this->assertTrue( is_a( $this->main->get_processor( 'email' ), 'LLMS_Notification_Processor_Email' ) ); - - // return false - $this->assertFalse( $this->main->get_processor( 'thisisveryveryfake' ) ); - - } - - /** - * test get_processors() method - * - * @since 3.8.0 - * @since 3.38.0 Use $this->main for code DRYness. - * - * @return void - */ - public function test_get_processors() { - - // should always return an array - $this->assertTrue( is_array( $this->main->get_processors() ) ); - - // each item in the array must extend the processor abstract - foreach ( $this->main->get_processors() as $processor ) { - $this->assertTrue( is_subclass_of( $processor, 'LLMS_Abstract_Notification_Processor' ) ); - } - - } - - /** - * Test schedule_processing() - * - * @since 3.38.0 - * - * @return void - */ - public function test_schedule_processing() { - - $expect = array( 'email' ); - - // Schedule. - $this->main->schedule_processing( 'email' ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'processors_to_dispatch' ) ); - - // Don't add duplicates. - $this->main->schedule_processing( 'email' ); - $this->assertEquals( $expect, LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'processors_to_dispatch' ) ); - - } - - /** - * Test schedule_processors_dispatch() - * - * @since 3.38.0 - * - * @return void - */ - public function test_schedule_processors_dispatch() { - - $now = time(); - llms_tests_mock_current_time( $now ); - - $this->main->schedule_processing( 'email' ); - $this->main->schedule_processing( 'fake-processor' ); - - $res = $this->main->schedule_processors_dispatch(); - - $this->assertArrayHaskey( 'email', $res ); - $this->assertArrayHaskey( 'fake-processor', $res ); - - $this->assertEquals( $now, $res['email'] ); - - $this->assertIsWPError( $res['fake-processor'] ); - $this->assertWPErrorCodeEquals( 'invalid-processor', $res['fake-processor'] ); - - } - - /** - * Test schedule_processors_dispatch() when none are scheduled - * - * @since 3.38.0 - * - * @return void - */ - public function test_schedule_processors_dispatch_none_scheduled() { - - $this->assertEquals( array(), $this->main->schedule_processors_dispatch() ); - - } - - /** - * Test schedule_single_processor() when an event is already scheduled - * - * @since 3.38.0 - * - * @return void - */ - public function test_schedule_single_processor_already_scheduled() { - - $email = $this->main->get_processor( 'email' )->push_to_queue( 1 ); - - // Schedule the event. - $orig = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) ); - - // Time travel. - llms_tests_mock_current_time( time() + HOUR_IN_SECONDS ); - - // Schedule the event again. - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) ); - - // Original timestamp should be returned. - $this->assertEquals( $orig, $res ); - - } - - /** - * Test schedule_single_processor() when an existing event does not already exist. - * - * @since 3.38.0 - * - * @return void - */ - public function test_schedule_single_processor_new() { - - $email = $this->main->get_processor( 'email' )->push_to_queue( 1 ); - - $now = time(); - llms_tests_mock_current_time( $now ); - - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) ); - $this->assertEquals( $now, $res ); - - } - -} diff --git a/tests/phpunit/unit-tests/notifications/controllers/class-llms-test-notification-controller-upcoming-payment-reminder.php b/tests/phpunit/unit-tests/notifications/controllers/class-llms-test-notification-controller-upcoming-payment-reminder.php deleted file mode 100644 index 854523dc0a..0000000000 --- a/tests/phpunit/unit-tests/notifications/controllers/class-llms-test-notification-controller-upcoming-payment-reminder.php +++ /dev/null @@ -1,239 +0,0 @@ -<?php -/** - * Upcoming Payment Reminder Notification Controller tests - * - * @package LifterLMS/Tests/Notifications/Controllers - * - * @group notification - * @group notification_controller - * - * @since 5.2.0 - */ -class LLMS_Test_Notification_Controller_Upcoming_Payment_Reminder extends LLMS_UnitTestCase { - - /** - * LLMS_Abstract_Notification_Controller extending class instance - * - * @var LLMS_Abstract_Notification_Controller - */ - private $controller; - - /** - * Supported notification types. - * - * @var string[] - */ - private $types; - - /** - * Consider dates equal within 60 seconds - * - * @var int - */ - private $date_delta = 60; - - /** - * Set up - * - * @since 5.2.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->controller = LLMS_Notification_Controller_Upcoming_Payment_Reminder::instance(); - $this->types = array_keys( $this->controller->get_supported_types() ); - } - - /** - * Test action_callback() method - * - * @since 5.2.0 - * - * @return void - */ - public function test_action_callback() { - - // Create order. - $order = $this->get_mock_order(); - // Create post. - $post_id = $this->factory->post->create(); - - $recurring_payments_site_feature = LLMS_Site::get_feature( 'recurring_payments' ); - - LLMS_Site::update_feature( 'recurring_payments', true ); - - // Check notification sent for existing order and student. - foreach ( $this->types as $type ) { - $this->assertTrue( $this->controller->action_callback( $order->get( 'id' ), $type ), $type ); - } - - // Check notification not sent for error gateway. - $order->set( 'payment_gateway', 'garbage' ); - foreach ( $this->types as $type ) { - $this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type ); - } - - // Check notification not sent for gateway that do not support recurring payments. - $manual = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - $manual->supports['recurring_payments'] = false; - $order->set( 'gateway', 'manual' ); - foreach ( $this->types as $type ) { - $this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type ); - } - - // Re-set recurring payments support for the manual gateway. - $manual->supports['recurring_payments'] = true; - - // Check notification not sent for unexisting order. - foreach ( $this->types as $type ) { - $this->assertFalse( $this->controller->action_callback( $order->get( 'id' ) + 1, $type ), $type ); - $this->assertFalse( $this->controller->action_callback( $post_id, $type ), $type ); - } - - // Check notication not sent for unexisting student. - $order->set( 'user_id', $order->get( 'user_id' ) + 1 ); - foreach ( $this->types as $type ) { - $this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type ); - } - - LLMS_Site::update_feature( 'recurring_payments', false ); - - // Check notification not sent for staging sites. - foreach ( $this->types as $type ) { - $this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type ); - } - - LLMS_Site::update_feature( 'recurring_payments', $recurring_payments_site_feature ); - - } - - /** - * Test get_upcoming_payment_reminder_test() - * - * @since 5.2.0 - * - * @return void - */ - public function test_get_upcoming_payment_reminder_test() { - - $plan = $this->get_mock_plan( 25.99, 1, 'lifetime', false, false ); - $plan->set( 'period', 'month' ); // Month. - $plan->set( 'length', 3 ); // for 3 total payments. - - $order = $this->get_mock_order( $plan ); - - $next_payment_date = $order->get_recurring_payment_due_date_for_scheduler(); - - // Reminder days (prior to the payment due date): default is 1. - foreach ( $this->types as $type ) { - $this->assertEquals( - strtotime( "-1 day", $next_payment_date ), - LLMS_Unit_Test_Util::call_method( - $this->controller, - 'get_upcoming_payment_reminder_date', - array( $order, $type ) - ), - $type - ); - } - - // Reminder days (prior to the payment due date): 10 and 11. - $i = 0; - foreach ( $this->types as $type ) { - - $days_option = LLMS_Unit_Test_Util::call_method( $this->controller, 'get_reminder_days', array( $type ) ); - $days = 10 + $i++; - $this->controller->set_option( $type . '_reminder_days', $days ); - - $this->assertEquals( - strtotime( "-{$days} day", $next_payment_date ), - LLMS_Unit_Test_Util::call_method( - $this->controller, - 'get_upcoming_payment_reminder_date', - array( $order, $type ) - ), - $type - ); - - $this->controller->set_option( $type . '_reminder_days', $days_option ); - } - - } - - /** - * Test schedule_upcoming_payment_reminder() - * - * @since 5.2.0 - * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter to `assertEquals()`. - * - * @return void - */ - public function test_schedule_upcoming_payment_reminder() { - - $plan = $this->get_mock_plan( 25.99, 1, 'lifetime', false, false ); - $plan->set( 'period', 'month' ); // Month. - $plan->set( 'length', 3 ); // for 3 total payments. - - $order = $this->get_mock_order( $plan ); - - // Upcoming payment reminders are unscheduled. - foreach ( $this->types as $type ) { - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - 'type' => $type, - ) - ), - $type - ); - } - - $next_payment_date = $order->get_recurring_payment_due_date_for_scheduler(); - - // Schedule. - $this->controller->schedule_upcoming_payment_reminders( $order, $next_payment_date ); - - // Check next payment reminder scheduled 1 day prior to payment due date. - foreach ( $this->types as $type ) { - $this->assertEqualsWithDelta( - (float) strtotime( "-1 day", $next_payment_date ), - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - 'type' => $type, - ) - ), - $this->date_delta, - $type - ); - } - - // Unschedule. - $this->controller->unschedule_upcoming_payment_reminders( $order ); - - // Fast forward. - llms_mock_current_time( date( 'Y-m-d', $next_payment_date + WEEK_IN_SECONDS ) ); - - // Try to schedule a notification that should be happen 1 week - 1 day in the past. - foreach ( $this->types as $type ) { - $this->assertWPErrorCodeEquals( 'upcoming-payment-reminder-passed', $this->controller->schedule_upcoming_payment_reminder( $order, $type, $next_payment_date ) ); - $this->assertFalse( - as_next_scheduled_action( - 'llms_send_upcoming_payment_reminder_notification', - array( - 'order_id' => $order->get( 'id' ), - 'type' => $type, - ) - ), - $type - ); - } - - } - -} diff --git a/tests/phpunit/unit-tests/processors/class-llms-test-processor-course-data.php b/tests/phpunit/unit-tests/processors/class-llms-test-processor-course-data.php deleted file mode 100644 index 48690e6571..0000000000 --- a/tests/phpunit/unit-tests/processors/class-llms-test-processor-course-data.php +++ /dev/null @@ -1,669 +0,0 @@ -<?php -/** - * Test Course data background processor - * - * @package LifterLMS/Tests - * - * @group processors - * @group processor_course_data - * - * @since 4.12.0 - */ -class LLMS_Test_Processor_Course_Data extends LLMS_UnitTestCase { - - /** - * Setup before class - * - * Forces processor debugging on so that we can make assertions against logged data. - * - * @since 4.12.0 - * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes. - * - * @return void - */ - public static function set_up_before_class() { - - parent::set_up_before_class(); - llms_maybe_define_constant( 'LLMS_PROCESSORS_DEBUG', true ); - - } - - /** - * Setup the test case - * - * @since 4.12.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - - $this->main = llms()->processors()->get( 'course_data' ); - $this->schedule_hook = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cron_hook_identifier' ); - - } - - /** - * Teardown the test case - * - * @since 4.12.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - $this->main->cancel_process(); - LLMS_Unit_Test_Util::set_private_property( $this->main, 'data', array() ); - parent::tear_down(); - - } - - /** - * Test dispatch_calc() when throttled by number of students - * - * @since 4.12.0 - * @since 4.21.0 Assert student enrolled count early. - * @since 5.2.1 Added 5 second delta on date comparison assertion. - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_dispatch_calc_throttled_by_students() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->factory->student->create_and_enroll_many( 2, $course_id ); - - // Clear things so scheduling works right. - wp_unschedule_event( wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 'llms_calculate_course_data', array( $course_id ) ); - $this->logs->clear( 'processors' ); - - // Fake throttling data. - LLMS_Unit_Test_Util::set_private_property( $this->main, 'throttle_max_students', 1 ); - $last_run = time() - HOUR_IN_SECONDS; - update_post_meta( $course_id, '_llms_last_data_calc_run', $last_run ); - - // Dispatch. - $this->main->dispatch_calc( $course_id ); - - /** - * Even if a course is throttled the student count should be updated right away since it's not only used for reporting - * - * @link https://github.com/gocodebox/lifterlms/issues/1564 - */ - $this->assertEquals( 2, get_post_meta( $course_id, '_llms_enrolled_students', true ) ); - - // Expected logs. - $logs = array( - "Course data calculation dispatched for course {$course_id}.", - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - "Course data calculation throttled for course {$course_id}.", - ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - // Event scheduled. - $this->assertEqualsWithDelta( $last_run + ( HOUR_IN_SECONDS * 4 ), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'throttle_max_students', 500 ); - - } - - /** - * Test dispatch_calc() when throttled because it's already processing for the course. - * - * @since 4.12.0 - * - * @return void - */ - public function test_dispatch_calc_throttled_by_course() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->factory->student->create_and_enroll_many( 1, $course_id ); - - update_post_meta( $course_id, '_llms_temp_calc_data_lock', 'yes' ); - - $this->logs->clear( 'processors' ); - - // Dispatch. - $this->main->dispatch_calc( $course_id ); - - // Expected logs. - $logs = array( - "Course data calculation dispatched for course {$course_id}.", - "Course data calculation triggered for course {$course_id}.", - "Course data calculation throttled for course {$course_id}.", - ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - } - - /** - * Test dispatch_calc() when there's no students in the course - * - * @since 4.21.0 - * @since 5.2.1 Added 5 second delta on date comparison assertion. - * - * @link https://github.com/gocodebox/lifterlms/issues/1596#issuecomment-821585937 - * - * @return void - */ - public function test_dispatch_calc_no_students() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $course = llms_get_post( $course_id ); - - // Mock meta data that may exist on the course (from a previous run, for example). - $metas = array( - 'average_grade' => array( 95, 0 ), - 'average_progress' => array( 22, 0 ), - 'enrolled_students' => array( 204, 0 ), - 'last_data_calc_run' => array( time() - HOUR_IN_SECONDS, time() ), - 'temp_calc_data' => array( array( 123 ), array() ), - ); - foreach ( $metas as $key => $vals ) { - $course->set( $key, $vals[0] ); - } - - $this->main->dispatch_calc( $course_id ); - - foreach ( $metas as $key => $vals ) { - $delta = 'last_data_calc_run' === $key ? 5 : 0; - $this->assertEqualsWithDelta( $vals[1], $course->get( $key ), $delta, $key ); - } - - } - - /** - * Test dispatch_calc() - * - * @since 4.12.0 - * @since 4.21.0 Assert student enrolled count early. - * - * @return void - */ - public function test_dispatch_calc_success() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->factory->student->create_and_enroll_many( 5, $course_id ); - $this->logs->clear( 'processors' ); - - $handler = function( $args ) { - $args['per_page'] = 2; - return $args; - }; - add_filter( 'llms_data_processor_course_data_student_query_args', $handler ); - - $this->main->dispatch_calc( $course_id ); - - /** - * Even if a course is throttled the student count should be updated right away since it's not only used for reporting - * - * @link https://github.com/gocodebox/lifterlms/issues/1564 - */ - $this->assertEquals( 5, get_post_meta( $course_id, '_llms_enrolled_students', true ) ); - - // Logged properly. - $this->assertEquals( array( "Course data calculation dispatched for course {$course_id}." ), $this->logs->get( 'processors' ) ); - - // Test data is loaded into the queue properly. - foreach ( LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' )->data as $i => $args ) { - - $this->assertEquals( $course_id, $args['post_id'] ); - $this->assertEquals( 2, $args['per_page'] ); - $this->assertEquals( array( 'enrolled' ), $args['statuses'] ); - $this->assertEquals( ++$i, $args['page'] ); - - } - - // Event scheduled. - $this->assertTrue( ! empty( wp_next_scheduled( $this->schedule_hook ) ) ); - - remove_filter( 'llms_data_processor_course_data_student_query_args', $handler ); - - } - - /** - * Test get_last_run() - * - * @since 4.12.0 - * - * @return void - */ - public function test_get_last_run() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_last_run', array( $course_id ) ) ); - - $now = time(); - update_post_meta( $course_id, '_llms_last_data_calc_run', $now ); - $this->assertEquals( $now, LLMS_Unit_Test_Util::call_method( $this->main, 'get_last_run', array( $course_id ) ) ); - - } - - /** - * Test get_task_data() - * - * @since 4.21.0 - * - * @return void - */ - public function test_get_task_data() { - - $data = array(); - - // Default data only. - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_task_data' ); - $this->assertEquals( array( - 'students' => 0, - 'progress' => 0, - 'quizzes' => 0, - 'grade' => 0, - ), $res ); - - - // Merge in some data - $merge = array( - 'progress' => 25, - 'students' => 203, - 'custom' => 'abc', - ); - $res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_task_data', array( $merge ) ); - $this->assertEquals( array( - 'students' => 203, - 'progress' => 25, - 'quizzes' => 0, - 'grade' => 0, - 'custom' => 'abc', - ), $res ); - - } - - /** - * Test is_already_processing_course() when it's not processing. - * - * @since 4.12.0 - * - * @return void - */ - public function test_is_already_processing_course() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $course = llms_get_post( $course_id ); - - // No meta data. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - // Unexpected / invalid meta values. - $course->set( 'temp_calc_data_lock', '' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - $course->set( 'temp_calc_data_lock', 'no' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - // Is running. - $course->set( 'temp_calc_data_lock', 'yes' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - - } - - /** - * Test maybe_throttle() - * - * @since 4.12.0 - * - * @return void - */ - public function test_maybe_throttle() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 25, $course_id ) ) ); - - // Hasn't run recently. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 500, $course_id ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 2500, $course_id ) ) ); - - // Should be throttled because of a recent run. - update_post_meta( $course_id, '_llms_last_data_calc_run', time() - HOUR_IN_SECONDS ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 500, $course_id ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 2500, $course_id ) ) ); - - } - - /** - * Test schedule_calculation() - * - * @since 4.21.0 - * @since 5.2.1 Added 5 second delta on date comparison assertions. - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_schedule_calculation() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $expected_time = time() + HOUR_IN_SECONDS; - $logs = array ( - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - ); - - // Schedule an event. - $this->main->schedule_calculation( $course_id, $expected_time ); - $this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - $this->logs->clear( 'processors' ); - - // No duplicate scheduled. - $this->main->schedule_calculation( $course_id ); - $this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - $this->assertEquals( array( $logs[0] ), $this->logs->get( 'processors' ) ); - - } - - /** - * Test schedule_calculation() to ensure duplicate events aren't scheduled regardless of ID variable type - * - * @since 4.21.0 - * @since 5.2.1 Added 5 second delta on date comparison assertions. - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @link https://github.com/gocodebox/lifterlms/issues/1600 - * - * @return void - */ - public function test_schedule_calculation_string_or_int() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $expected_time = time() + HOUR_IN_SECONDS; - $logs = array ( - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - ); - - // Schedule with an int. - $this->main->schedule_calculation( $course_id, $expected_time ); - $this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - $this->logs->clear( 'processors' ); - - // No duplicate should be scheduled if using a string later. - $this->main->schedule_calculation( (string) $course_id ); - $this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - $this->assertEquals( array( $logs[0] ), $this->logs->get( 'processors' ) ); - - } - - /** - * Test schedule_from_course() - * - * @since 4.12.0 - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_schedule_from_course() { - - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - $this->main->schedule_from_course( 123, $course_id ); - - // Logs. - $logs = array ( - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - // Event. - $this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - - } - - /** - * Test schedule_from_lesson() - * - * @since 4.12.0 - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_schedule_from_lesson() { - - $course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1 ) ); - $lesson_id = llms_get_post( $course_id )->get_lessons( 'ids' )[0]; - - $this->main->schedule_from_lesson( 123, $lesson_id ); - - // Logs. - $logs = array ( - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - // Event. - $this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - - } - - /** - * Test schedule_from_quiz() - * - * @since 4.12.0 - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_schedule_from_quiz() { - - $course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1 ) ); - $quiz_id = llms_get_post( $course_id )->get_lessons()[0]->get( 'quiz' ); - $student_id = $this->factory->student->create(); - $attempt = $this->take_quiz( $quiz_id, $student_id ); - - $this->main->schedule_from_quiz( $student_id, $quiz_id, $attempt ); - - // Logs. - // In this particular test the process is already running because of the lesson completion triggered by the quiz. - // This does not render the trigger entirely useless though as the quiz itself could trigger without lessons - // when using add-ons that implement restrictions on lesson progression. - $logs = array ( - "Course data calculation triggered for course {$course_id}.", - "Course data calculation scheduled for course {$course_id}.", - "Course data calculation triggered for course {$course_id}.", - "Course data calculation triggered for course {$course_id}.", - ); - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - // Event. - $this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 ); - - } - - /** - * Test task() method - * - * @since 4.12.0 - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_task() { - - $course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 2, 'quizzes' => 1 ) ); - $course = llms_get_post( $course_id ); - $students = $this->factory->student->create_and_enroll_many( 5, $course_id ); - - foreach ( $students as $i => $student ) { - $perc = array( 0, 50, 50, 100, 100 ); - $this->complete_courses_for_student( $student, $course_id, $perc[ $i ] ); - } - - // Clear any data that may exist as a result of mock data creation above. - delete_post_meta( $course_id, '_llms_temp_calc_data' ); - - // Perform task for page 1, not completed, save the data. - $this->assertFalse( $this->main->task( array( - 'post_id' => $course_id, - 'statuses' => array( 'enrolled' ), - 'page' => 1, - 'per_page' => 2, - 'sort' => array( - 'id' => 'ASC', - ), - ) ) ); - - $expect = array( - 'students' => 2, - 'progress' => floatval( 50 ), - 'quizzes' => 0, - 'grade' => 0, - ); - $this->assertEquals( $expect, $course->get( 'temp_calc_data' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - - // Perform task for page 2, not completed, save the data. - $this->assertFalse( $this->main->task( array( - 'post_id' => $course_id, - 'statuses' => array( 'enrolled' ), - 'page' => 2, - 'per_page' => 2, - 'sort' => array( - 'id' => 'ASC', - ), - ) ) ); - - $expect = array( - 'students' => 4, - 'progress' => floatval( 200 ), - 'quizzes' => 1, - 'grade' => floatval( 100 ), - ); - $this->assertEquals( $expect, $course->get( 'temp_calc_data' ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - - // Perform task for page 3, completed. - $this->assertFalse( $this->main->task( array( - 'post_id' => $course_id, - 'statuses' => array( 'enrolled' ), - 'page' => 3, - 'per_page' => 2, - 'sort' => array( - 'id' => 'ASC', - ), - ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) ); - $this->assertEmpty( $course->get( 'temp_calc_data' ) ); - $this->assertEmpty( $course->get( 'temp_calc_data_lock' ) ); - $this->assertEquals( 100, $course->get( 'average_grade' ) ); - $this->assertEquals( 60, $course->get( 'average_progress' ) ); - $this->assertEquals( 5, $course->get( 'enrolled_students' ) ); - $this->assertEqualsWithDelta( time(), $course->get( 'last_data_calc_run' ), 5 ); - - } - - /** - * Test deleted / nonexistant courses/posts. - * - * @since 4.21.0 - * - * @return void - */ - public function test_task_nonexistent_course() { - - $tests = array( - // Deleted course. - $this->factory->post->create( array( 'post_type' => 'course' ) ), - - // Not a course. - $this->factory->post->create(), - ); - - wp_delete_post( $tests[0], true ); - - // Not a real post at all. - $tests[] = $tests[1] + 1; - - foreach ( $tests as $post_id ) { - - $args = compact( 'post_id' ); - $this->assertFalse( $this->main->task( $args ) ); - - $json = wp_json_encode( $args ); - - $logs = array ( - "Course data calculation task called for course {$post_id} with args: {$json}", - "Course data calculation task skipped for course {$post_id}.", - ); - - $this->assertEquals( $logs, $this->logs->get( 'processors' ) ); - - $this->logs->clear( 'processors' ); - - } - - } - - - /** - * Test dispatch_calc() with multiple courses to make sure that tasks are not duplicated in other batches. - * - * @since 4.21.0 - * - * @link https://github.com/gocodebox/lifterlms/issues/1602 - * - * @return void - */ - public function test_duplicate_batch_tasks() { - - $course_ids[] = $this->factory->post->create( array( 'post_type' => 'course' ) ); - $course_ids[] = $this->factory->post->create( array( 'post_type' => 'course' ) ); - foreach ( $course_ids as $course_id ) { - $this->factory->student->create_and_enroll_many( 5, $course_id ); - } - $this->logs->clear( 'processors' ); - - $handler = function ( $args ) { - $args['per_page'] = 2; - - return $args; - }; - add_filter( 'llms_data_processor_course_data_student_query_args', $handler ); - - $expected_logs = array(); - foreach ( $course_ids as $course_id ) { - $this->main->dispatch_calc( $course_id ); - $expected_logs[] = "Course data calculation dispatched for course {$course_id}."; - } - // Logged properly. - $this->assertEquals( $expected_logs, $this->logs->get( 'processors' ) ); - - foreach ( $course_ids as $course_id ) { - $batch = LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' ); - - // Test data is loaded into the queue properly. - foreach ( $batch->data as $i => $student_query_args ) { - $this->assertEquals( $course_id, $student_query_args['post_id'], $course_id ); - $this->assertEquals( 2, $student_query_args['per_page'], 'per_page' ); - $this->assertEquals( array( 'enrolled' ), $student_query_args['statuses'], 'statuses' ); - $this->assertEquals( ++ $i, $student_query_args['page'], 'page' ); - } - - // Simulate handling of queued batched tasks. - LLMS_Unit_Test_Util::call_method( $this->main, 'delete', array( $batch->key ) ); - } - - remove_filter( 'llms_data_processor_course_data_student_query_args', $handler ); - - } - -} diff --git a/tests/phpunit/unit-tests/processors/class-llms-test-processors.php b/tests/phpunit/unit-tests/processors/class-llms-test-processors.php deleted file mode 100644 index 2eed94274a..0000000000 --- a/tests/phpunit/unit-tests/processors/class-llms-test-processors.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php -/** - * Test LLMS_Processors - * - * @package LifterLMS/Tests - * - * @group processors - * - * @since 5.0.0 - */ -class LLMS_Test_Processors extends LLMS_Unit_Test_Case { - - /** - * Setup test case - * - * @since 5.0.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->main = LLMS_Processors::instance(); - } - - /** - * Test `instance()`. - * - * @since 5.0.0 - * @since 5.3.0 Rename `_instance` property to `instance`. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @return void - */ - public function test_instance() { - - $this->main->fake = 'mock'; - $this->assertEquals( $this->main, LLMS_Processors::instance() ); - - LLMS_Unit_Test_Util::set_private_property( $this->main, 'instance', null ); - if ( property_exists( $this->main, '_instance' ) ) { - LLMS_Unit_Test_Util::set_private_property( $this->main, '_instance', null ); - } - $new_instance = LLMS_Processors::instance(); - $this->assertInstanceOf( 'LLMS_Processors', $new_instance ); - $this->assertTrue( ! isset( $new_instance->fake ) ); - - } - - /** - * Test get() - * - * @since 5.0.0 - * - * @return void - */ - public function test_get() { - - $this->assertInstanceOf( 'LLMS_Processor_Course_Data', $this->main->get( 'course_data' ) ); - $this->assertFalse( $this->main->get( 'fake' ) ); - - } - - /** - * Test load_processor() - * - * @since 5.0.0 - * - * @return void - */ - public function test_load_processor() { - - $this->assertTrue( $this->main->load_processor( 'course_data' ) ); - $this->assertFalse( $this->main->load_processor( 'fake' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-checkout.php b/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-checkout.php deleted file mode 100644 index 9aee0c962d..0000000000 --- a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-checkout.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Test the [lifterlms_checkout] Shortcode - * - * @group shortcodes - * - * @since 5.1.0 - * @version 5.1.0 - */ -class LLMS_Test_Shortcode_Checkout extends LLMS_ShortcodeTestCase { - - /** - * Test shortcode registration - * - * @since 5.1.0 - * - * @return void - */ - public function test_registration() { - $this->assertTrue( shortcode_exists( 'lifterlms_checkout' ) ); - } - - /** - * Test clean_form_fields - * - * @since 5.1.0 - * - * @return void - */ - public function test_clean_form_fields() { - - $checks = array( - '<p></p>' => '', - '<p>a</p>' => '<p>a</p>', - "\n" => '', - "\t" => '', - "\n\r\t" => '', - "<p></p>\n<p>a</p>\r\t" => "<p></p>\n<p>a</p>\r\t", - ); - - foreach ( $checks as $check => $expect ) { - $this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcode_Checkout', 'clean_form_fields', array( $check ) ), $check ); - } - - } -} diff --git a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-course-progress.php b/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-course-progress.php deleted file mode 100644 index 6918cbe8dd..0000000000 --- a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-course-progress.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * Test the [lifterlms_course_progress] Shortcode - * - * @group shortcodes - * - * @since 3.38.0 - * @version 3.38.0 - */ -class LLMS_Test_Shortcode_Course_Progress extends LLMS_ShortcodeTestCase { - - /** - * Test shortcode registration - * - * @since 3.38.0 - * - * @return void - */ - public function test_registration() { - $this->assertTrue( shortcode_exists( 'lifterlms_course_progress' ) ); - } - - /** - * Test shortcode output - * - * @since 3.38.0 - * - * @return void - */ - public function test_get_output() { - - $course = $this->factory->post->create( array( - 'post_type' => 'course', - ) ); - - // Alter some globals just to emulate we're in a singular course. - global $post, $wp_query; - $temp_q = $wp_query; - $temp_p = $post; - - $wp_query->queried_object = get_post($course); - $wp_query->is_singular = true; - $post = $wp_query->queried_object; - - - $expected_shortcode = '<div class="llms-progress"> - <div class="progress__indicator">0%</div> - <div class="llms-progress-bar"> - <div class="progress-bar-complete" data-progress="0%" style="width:0%"></div> - </div></div>'; - - // Test against logged out user. - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' ); - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' ); - - // Progress should not be shown to logged out users if check_enrollment=1. - $this->assertShortcodeOutputEquals( '', '[lifterlms_course_progress check_enrollment=1]' ); - - // Get a student and try again - $student = $this->get_mock_student( true ); - - $student->enroll( $course ); - - // Student enrolled: progress should always be shown - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' ); - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' ); - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=1]' ); - - $student->unenroll( $course ); - // Student unenrolled but logged in: same as logged out. - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' ); - $this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' ); - $this->assertShortcodeOutputEquals( '', '[lifterlms_course_progress check_enrollment=1]' ); - - // Reset globals alterations. - $wp_query = $temp_q; - $post = $temp_p; - - } - -} diff --git a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-hide-content.php b/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-hide-content.php deleted file mode 100644 index a9c3365383..0000000000 --- a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-hide-content.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Test the [lifterlms_hide_content] Shortcode - * @group shortcodes - * @since 3.24.1 - * @version 3.30.2 - */ -class LLMS_Test_Shortcode_Hide_Content extends LLMS_ShortcodeTestCase { - - /** - * Class name of the Shortcode Class - * @var string - */ - public $class_name = 'LLMS_Shortcode_Hide_Content'; - - public function test_get_output() { - - // Test against logged out user. - $this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id="1"]Secrets.[/lifterlms_hide_content]' ); - - // Logged out with multiples & different relationships. - $this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id="1,2,3,4" relation="any"]Secrets.[/lifterlms_hide_content]' ); - $this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id="1,2,3,4" relation="all"]Secrets.[/lifterlms_hide_content]' ); - - // Show a message - $this->assertShortcodeOutputEquals( 'Nope.', '[lifterlms_hide_content id="1" message="Nope."]Secrets.[/lifterlms_hide_content]' ); - $this->assertShortcodeOutputEquals( 'Nope.', '[lifterlms_hide_content id="1,2,3,4" message="Nope." relation="any"]Secrets.[/lifterlms_hide_content]' ); - - - // get a student and try again - $student = $this->get_mock_student( true ); - - // check against both courses and memberships - foreach ( array( 'course', 'llms_membership' ) as $post_type ) { - - $ids = $this->factory->post->create_many( 3, array( - 'post_type' => $post_type, - ) ); - - // enroll only in the first. - $student->enroll( $ids[0] ); - - // Can see secrets b/c enrollment. - $this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id="%d"]Secrets.[/lifterlms_hide_content]', $ids[0] ) ); - - // Cannot see b/c no enrollment. - $this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id="%d"]Secrets.[/lifterlms_hide_content]', $ids[1] ) ); - $this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id="%d"]Secrets.[/lifterlms_hide_content]', $ids[2] ) ); - - // Must belong to all and does not. - $this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id="%s" relation="all"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[1] ) ); - - // Must belong to any and only belongs to one. - $this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id="%s" relation="any"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[1] ) ); - - // Enroll in another - $student->enroll( $ids[2] ); - - // Check two, belongs to both. - $this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id="%s" relation="all"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[2] ) ); - - // Check three. - $this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id="%s" relation="all"]Secrets.[/lifterlms_hide_content]', implode( ',', $ids ) ) ); - - // Check any of the two (belongs to both). - $this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id="%s" relation="all"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[2] ) ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-user-info.php b/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-user-info.php deleted file mode 100644 index fd86e6d566..0000000000 --- a/tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-user-info.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * Test the User Info shortcode - * - * @package LifterLMS/Tests - * - * @group shortcodes - * @group userinfo_shortcode - * - * @since 5.0.0 - * @version 5.0.0 - */ -class LLMS_Test_Shortcode_User_Info extends LLMS_ShortcodeTestCase { - - /** - * Class name of the Shortcode Class - * @var string - */ - public $class_name = 'LLMS_Shortcode_User_Info'; - - /** - * Test setting attributes with no key for the first attribute (field name). - * - * @since 5.0.0 - * - * @return void - */ - public function test_set_attributes_field_no_key() { - - $obj = $this->get_class(); - - $atts = array( - 0 => 'first_name', - 'or' => 'mock', - ); - - $this->assertArrayHasKey( 'key', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) ); - $this->assertArrayHasKey( 'or', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) ); - - } - - /** - * Test setting attributes when the field name is passed. - * - * @since 5.0.0 - * - * @return void - */ - public function test_set_attributes_field_regular() { - - $obj = $this->get_class(); - - $atts = array( - 'key' => 'first_name', - ); - $this->assertArrayHasKey( 'key', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) ); - - } - - /** - * Test get_output() with logged out user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_output_no_user() { - - $this->assertShortcodeOutputEquals( '', '[llms-user first_name]' ); - $this->assertShortcodeOutputEquals( 'Pal', '[llms-user first_name or="Pal"]' ); - - } - - /** - * Test get_output() with logged in user. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_output_with_user() { - - $user = $this->factory->user->create_and_get(); - wp_set_current_user( $user->ID ); - - // No value set. - $this->assertShortcodeOutputEquals( 'Bucko', '[llms-user first_name or="Bucko"]' ); - - update_user_meta( $user->ID, 'first_name', 'mock' ); - $this->assertShortcodeOutputEquals( 'mock', '[llms-user first_name]' ); - - // Works. - $this->assertShortcodeOutputEquals( $user->ID, '[llms-user ID]' ); - $this->assertShortcodeOutputEquals( $user->display_name, '[llms-user display_name]' ); - $this->assertShortcodeOutputEquals( $user->user_email, '[llms-user user_email]' ); - - // Blocked. - $this->assertShortcodeOutputEquals( '', '[llms-user user_pass]' ); - - update_user_meta( $user->ID, 'llms_phone', '123456789' ); - $this->assertShortcodeOutputEquals( '123456789', '[llms-user llms_phone]' ); - - } - - /** - * Test output when filtering the user to display another user's information - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_output_for_another() { - - $user = $this->factory->user->create_and_get(); - - $handler = function( $uid ) use( $user ) { - return $user->ID; - }; - add_filter( 'llms_user_info_shortcode_user_id', $handler ); - - // Works. - $this->assertShortcodeOutputEquals( $user->ID, '[llms-user ID]' ); - $this->assertShortcodeOutputEquals( $user->display_name, '[llms-user display_name]' ); - $this->assertShortcodeOutputEquals( $user->user_email, '[llms-user user_email]' ); - - remove_filter( 'llms_user_info_shortcode_user_id', $handler ); - - } - -} diff --git a/tests/phpunit/unit-tests/tables/class-llms-test-table-quizzes.php b/tests/phpunit/unit-tests/tables/class-llms-test-table-quizzes.php deleted file mode 100644 index 21857741a5..0000000000 --- a/tests/phpunit/unit-tests/tables/class-llms-test-table-quizzes.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Test the quizzes reporting table. - * - * @package LifterLMS/Tests/Tables - * - * @group reporting_tables - * - * @since 3.36.1 - */ -class LLMS_Test_Table_Quizzes extends LLMS_UnitTestCase { - - /** - * Setup test. - * - * @since 3.36.1 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/llms.table.quizzes.php'; - $this->table = new LLMS_Table_Quizzes(); - - } - - /** - * test quizzes table is empty for instructors with no courses or courses with no lessons - * - * @since 3.36.1 - * - * @return void - */ - public function test_no_quizzes_for_instructor_with_no_course_lesson() { - - // Setup a course with lessons and quizzes. - $course = $this->factory->course->create_and_get( array( - 'sections' => 1, - 'lessons' => 3, - 'quizzes' => 2, - ) ); - - // Setup an instructor. - $instructor_id = $this->factory->instructor->create(); - - wp_set_current_user( $instructor_id ); - - // The instructor has no courses, we expect no data. - $table = new LLMS_Table_Quizzes(); - $table->get_results(); - $this->assertEquals( 0, count( $table->get_tbody_data() ) ); - - // Setup a course with no lessons and assign the instructor to the course. - $inst_course = $this->factory->course->create_and_get( array( - 'sections' => 1, - 'lessons' => 0, - ) ); - $inst_course->instructors()->set_instructors(array( - array( - 'id' => $instructor_id, - ), - )); - - // The instructor has a course, but the course has no lessons, we expect no data. - $table = new LLMS_Table_Quizzes(); - $table->get_results(); - $this->assertEquals( 0, count( $table->get_tbody_data() ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/tables/class-llms-test-table-students.php b/tests/phpunit/unit-tests/tables/class-llms-test-table-students.php deleted file mode 100644 index f5757fba20..0000000000 --- a/tests/phpunit/unit-tests/tables/class-llms-test-table-students.php +++ /dev/null @@ -1,272 +0,0 @@ -<?php -/** - * Test the students reporting table. - * - * @package LifterLMS/Tests/Tables - * - * @group reporting_tables - * - * @since 3.28.0 - */ -class LLMS_Test_Table_Students extends LLMS_UnitTestCase { - - /** - * Setup test - * - * @since 3.28.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - require_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/llms.table.students.php'; - $this->table = new LLMS_Table_Students(); - - } - - - /** - * test the get_export() method. - * - * @since 3.28.0 - * - * @return void - */ - public function test_get_export() { - - // Enroll a bunch of students. - $this->factory->student->create_and_enroll_many( 10, $this->factory->course->create() ); - - // Setup an admin user - $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $admin_id ); - - $table = new LLMS_Table_Students(); - $export = $table->get_export(); - $this->assertTrue( count( $export ) >= 11 ); - $this->assertEquals( $table->get_export_header(), $export[0] ); - - } - - /** - * test the generate_export_file() method. - * - * @return void - * @since 3.28.0 - * @version 3.28.1 - */ - public function test_generate_export_file() { - - // Create a course. - $course = $this->factory->course->create_and_get(); - - // Enroll a bunch of students. - $this->factory->student->create_and_enroll_many( 50, $course->get( 'id' ) ); - - // Setup an instructor. - $instructor_id = $this->factory->instructor->create(); - $course->instructors()->set_instructors( array( array( 'id' => $instructor_id ) ) ); - wp_set_current_user( $instructor_id ); - - // unboost to make testing faster. - add_filter( 'llms_table_generate_export_file_per_page_boost', function() { - return 25; - } ); - - $table = new LLMS_Table_Students(); - $file = $table->generate_export_file(); - - $this->assertTrue( file_exists( LLMS_TMP_DIR . $file['filename'] ) ); - $this->assertEquals( 50, $file['progress'] ); - - $file = $table->generate_export_file( array(), $file['filename'] ); - $this->assertEquals( 100, $file['progress'] ); - - } - - /** - * Test generate_export_file(): prevent invalid filetypes. - * - * @since 3.37.15 - * - * @return void - */ - public function test_generate_export_file_invalid_file_type() { - - $table = new LLMS_Table_Students(); - - // No. - $this->assertFalse( $table->generate_export_file( array(), 'f.php' ) ); - - // Okay. - $this->assertTrue( is_array( $table->generate_export_file( array(), 'ok.csv' ) ) ); - $this->assertTrue( is_array( $table->generate_export_file( ) ) ); - - } - - /** - * test the get_results() method. - * - * @since 3.28.0 - * - * @return void - */ - public function test_get_results() { - - $checks = array( - array( - 'key' => 'page', - 'func' => 'get_current_page', - 'default' => 1, - 'change' => 2, - ), - array( - 'key' => 'order', - 'func' => 'get_order', - 'default' => 'ASC', - 'change' => 'DESC', - ), - array( - 'key' => 'orderby', - 'func' => 'get_orderby', - 'default' => 'name', - 'change' => 'id', - ), - array( - 'key' => 'per_page', - 'func' => 'get_per_page', - 'default' => 25, - 'change' => 5, - ), - ); - - $result_args = wp_list_pluck( $checks, 'change', 'key' ); - - // Setup course. - $course = $this->factory->course->create_and_get(); - - // Enroll a bunch of students. - $this->factory->student->create_and_enroll_many( 10, $course->get( 'id' ) ); - - // Current user has no access to anything. - $table = new LLMS_Table_Students(); - $table->get_results(); - $this->assertEmpty( $table->get_tbody_data() ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['default'], $table->{ $data['func'] }() ); - } - $table->get_results( $result_args ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['default'], $table->{ $data['func'] }() ); - } - - // Setup an instructor. - $instructor_id = $this->factory->instructor->create(); - $course->instructors()->set_instructors( array( array( 'id' => $instructor_id ) ) ); - - wp_set_current_user( $instructor_id ); - $table = new LLMS_Table_Students(); - $table->get_results(); - $this->assertEquals( 10, count( $table->get_tbody_data() ) ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['default'], $table->{ $data['func'] }() ); - } - $table->get_results( $result_args ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['change'], $table->{ $data['func'] }() ); - } - $this->assertEquals( 2, $table->get_max_pages() ); - $this->assertTrue( $table->is_last_page() ); - - $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $admin_id ); - $table = new LLMS_Table_Students(); - $table->get_results(); - $this->assertTrue( count( $table->get_tbody_data() ) >= 10 ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['default'], $table->{ $data['func'] }() ); - } - $table->get_results( $result_args ); - foreach ( $checks as $data ) { - $this->assertEquals( $data['change'], $table->{ $data['func'] }() ); - } - $this->assertTrue( $table->get_max_pages() >= 2 ); - - } - - /** - * Test the set_args() method. - * - * @since 3.28.0 - * - * @return void - */ - public function test_set_args() { - - $this->assertEquals( array( 'per_page' => 25 ), $this->table->set_args() ); - - } - - /** - * Test the set_columns() method - * - * @since 3.28.0 - * @since 3.36.0 Add "last_seen" col. - * - * @return void - */ - public function test_set_columns() { - - $cols = $this->table->set_columns(); - $this->assertTrue( is_array( $cols ) ); - $this->assertEquals( 27, count( $cols ) ); - $this->assertEquals( array ( - 'id', - 'email', - 'name', - 'name_last', - 'name_first', - 'registered', - 'last_seen', - 'overall_progress', - 'overall_grade', - 'enrollments', - 'completions', - 'certificates', - 'achievements', - 'memberships', - 'billing_address_1', - 'billing_address_2', - 'billing_city', - 'billing_state', - 'billing_zip', - 'billing_country', - 'phone', - 'courses_enrolled', - 'courses_cancelled', - 'courses_expired', - 'memberships_enrolled', - 'memberships_cancelled', - 'memberships_expired', - ), array_keys( $cols ) ); - - } - - /** - * Test that variables are setup correctly during construction. - * - * @since 3.28.0 - * - * @return void - */ - public function test_variables() { - - $this->assertEquals( 'Students', $this->table->get_title() ); - $this->table->set( 'title', 'Something Else' ); - $this->assertEquals( 'Something Else', $this->table->get_title() ); - - } - -} diff --git a/tests/phpunit/unit-tests/theme-support/class-llms-test-theme-support.php b/tests/phpunit/unit-tests/theme-support/class-llms-test-theme-support.php deleted file mode 100644 index 664c9f6dcd..0000000000 --- a/tests/phpunit/unit-tests/theme-support/class-llms-test-theme-support.php +++ /dev/null @@ -1,159 +0,0 @@ -<?php -/** - * Test LLMS_Theme_Support - * - * @package LifterLMS/Tests - * - * @group theme_support - * - * @since 3.37.0 - * @since 4.10.0 Added tests for Twenty Twenty-One theme. - * @since 5.9.0 Added tests for Twenty Twenty-Two - */ -class LLMS_Test_Theme_Support extends LLMS_Unit_Test_Case { - - /** - * Array of supported themes - * - * template => support class name. - * - * @var array - */ - protected $supported = array( - 'twentynineteen' => 'LLMS_Twenty_Nineteen', - 'twentytwenty' => 'LLMS_Twenty_Twenty', - 'twentytwentyone' => 'LLMS_Twenty_Twenty_One', - 'twentytwentytwo' => 'LLMS_Twenty_Twenty_Two', - ); - - /** - * Test get_css() - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_css() { - $this->assertEquals( 'body, .el { background: red; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red' ) ) ); - $this->assertEquals( 'body, .el { background: red; color: black; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red', 'color' => 'black' ) ) ); - } - - - /** - * Test get_css() with a prefix - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_css_with_prefix() { - $this->assertEquals( '#prefix body, #prefix .el { background: red; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red' ), '#prefix' ) ); - $this->assertEquals( '#prefix body, #prefix .el { background: red; color: black; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red', 'color' => 'black' ), '#prefix' ) ); - } - - /** - * Test get_css() when passing in an array for a rule - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_css_with_array_of_rules() { - - $css = array( - 'background-image' => array( - '-webkit-radial-gradient(fake)', - 'radial-gradient(fake)', - ) - ); - - $expected = '#prefix body, #prefix .el { background-image: -webkit-radial-gradient(fake); background-image: radial-gradient(fake); }'; - - $this->assertEquals( $expected, LLMS_Theme_Support::get_css( array( 'body', '.el' ), $css, '#prefix' ) ); - } - - /** - * Test get_selectors_primary_color_background() - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_selectors_primary_color_background() { - $res = LLMS_Theme_Support::get_selectors_primary_color_background(); - $this->assertTrue( is_array( $res ) ); - foreach ( $res as $sel ) { - $this->assertTrue( is_string( $sel ) ); - } - } - - /** - * Test get_selectors_primary_color_border() - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_selectors_primary_color_border() { - $res = LLMS_Theme_Support::get_selectors_primary_color_border(); - $this->assertTrue( is_array( $res ) ); - foreach ( $res as $sel ) { - $this->assertTrue( is_string( $sel ) ); - } - } - - /** - * Test get_selectors_primary_color_text() - * - * @since 4.10.0 - * - * @return void - */ - public function test_get_selectors_primary_color_text() { - $res = LLMS_Theme_Support::get_selectors_primary_color_text(); - $this->assertTrue( is_array( $res ) ); - foreach ( $res as $sel ) { - $this->assertTrue( is_string( $sel ) ); - } - } - - - /** - * Test theme support classes are loaded based on the current theme template. - * - * @since 3.37.0 - * - * @return void - */ - public function test_includes_no_support() { - - update_option( 'template', 'default' ); - - foreach ( array_values( $this->supported ) as $class ) { - $this->assertTrue( ! class_exists( $class ) ); - } - - } - - /** - * Test theme support classes are loaded based on the current theme template. - * - * @since 3.37.0 - * @since 4.3.0 Update theme support class instantiation. - * - * @return void - */ - public function test_includes_with_support() { - - foreach ( $this->supported as $template => $class ) { - update_option( 'template', $template ); - $support = new LLMS_Theme_Support(); - $support->includes(); - $this->assertTrue( class_exists( $class ) ); - } - - update_option( 'template', 'default' ); - - } - -} diff --git a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-one.php b/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-one.php deleted file mode 100644 index cd18b0add0..0000000000 --- a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-one.php +++ /dev/null @@ -1,205 +0,0 @@ -<?php -/** - * Test LLMS_Twenty_Twenty theme support class - * - * @package LifterLMS/Tests - * - * @group theme_support - * - * @since 4.10.0 - */ -class LLMS_Test_Twenty_Twenty_One extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - update_option( 'template', 'twentytwentyone' ); - $support = new LLMS_Theme_Support(); - $support->includes(); - - } - - /** - * Tear down the test case. - * - * @since 4.10.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - update_option( 'template', 'default' ); - - } - - /** - * Remove all the header actions setup by `handle_page_header_wrappers()`. - * - * @since 4.10.0 - * - * @return void - */ - protected function remove_header_actions() { - - remove_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ), 11 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ), 99999999 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ), -1 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ), 99999998 ); - - } - - /** - * Test add_max_width_class() - * - * @since 4.10.0 - * - * @return void - */ - public function test_add_max_width_class() { - $this->assertEquals( array( 'mock-class', 'default-max-width' ), LLMS_Twenty_Twenty_One::add_max_width_class( array( 'mock-class' ) ) ); - } - - /** - * Test add_pagination_classes() - * - * @since 4.10.0 - * - * @return void - */ - public function test_add_pagination_classes() { - $this->assertEquals( array( 'mock-class', 'navigation', 'pagination' ), LLMS_Twenty_Twenty_One::add_pagination_classes( array( 'mock-class' ) ) ); - } - - /** - * Test handle_page_header_wrappers() when the archive title is disabled. - * - * @since 4.10.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_no_title() { - - $this->remove_header_actions(); - add_filter( 'lifterlms_show_page_title', '__return_false' ); - - LLMS_Twenty_Twenty_One::handle_page_header_wrappers(); - - $this->assertFalse( has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) ); - - remove_filter( 'lifterlms_show_page_title', '__return_false' ); - - } - - /** - * Test handle_page_header_wrappers() when there's no archive description - * - * @since 4.10.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_no_desc() { - - $this->remove_header_actions(); - - LLMS_Twenty_Twenty_One::handle_page_header_wrappers(); - - $this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) ); - $this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) ); - - $this->remove_header_actions(); - - } - - /** - * Test handle_page_header_wrappers() when there is an archived description - * - * @since 4.10.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_title_and_desc() { - - $this->remove_header_actions(); - - // Output a description. - $handler = function( $desc ) { - return 'Archive description'; - }; - add_filter( 'llms_archive_description', $handler ); - - LLMS_Twenty_Twenty_One::handle_page_header_wrappers(); - - $this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) ); - $this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) ); - $this->assertEquals( -1, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) ); - $this->assertEquals( 99999998, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) ); - - $this->remove_header_actions(); - - remove_filter( 'llms_archive_description', $handler ); - - } - - /** - * Test modify_columns_count() - * - * @since 4.10.0 - * - * @return void - */ - public function test_modify_columns_count() { - - $this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 1 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 2 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 3 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 999 ) ); - - } - - /** - * Test maybe_disable_post_navigation() - * - * @since 4.10.0 - * - * @return void - */ - public function test_maybe_disable_post_navigation() { - - global $post; - $temp = $post; - - $tests = array( - 'post' => 'default html', - 'course' => '', - 'llms_membership' => '', - 'lesson' => '', - 'llms_quiz' => '', - ); - - foreach ( $tests as $post_type => $expected ) { - - $post = $this->factory->post->create( compact( 'post_type' ) ); - $this->assertEquals( $expected, LLMS_Twenty_Twenty_One::maybe_disable_post_navigation( 'default html' ) ); - - } - - $post = $temp; - - } - -} diff --git a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-two.php b/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-two.php deleted file mode 100644 index cba6fe43a1..0000000000 --- a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-two.php +++ /dev/null @@ -1,152 +0,0 @@ -<?php -/** - * Test LLMS_Twenty_Twenty_Two theme support class - * - * @package LifterLMS/Tests - * - * @group theme_support - * @group twenty_twenty_two - * - * @since 5.8.0 - */ -class LLMS_Test_Twenty_Twenty_Two extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 5.8.0 - * - * @return void - */ - public function set_up() { - - parent::set_up(); - update_option( 'template', 'twentytwentytwo' ); - $support = new LLMS_Theme_Support(); - $support->includes(); - - } - - /** - * Tear down the test case. - * - * @since 5.8.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - update_option( 'template', 'default' ); - - } - - /** - * Remove all the header actions setup by `handle_page_header_wrappers()`. - * - * @since 5.8.0 - * - * @return void - */ - protected function remove_header_actions() { - - remove_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ), 11 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ), 99999999 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ), -1 ); - remove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ), 99999998 ); - - } - - /** - * Test handle_page_header_wrappers() when the archive title is disabled. - * - * @since 5.8.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_no_title() { - - $this->remove_header_actions(); - add_filter( 'lifterlms_show_page_title', '__return_false' ); - - LLMS_Twenty_Twenty_Two::handle_page_header_wrappers(); - - $this->assertFalse( has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) ); - - remove_filter( 'lifterlms_show_page_title', '__return_false' ); - - } - - /** - * Test handle_page_header_wrappers() when there's no archive description - * - * @since 5.8.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_no_desc() { - - $this->remove_header_actions(); - - LLMS_Twenty_Twenty_Two::handle_page_header_wrappers(); - - $this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) ); - $this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) ); - $this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) ); - - $this->remove_header_actions(); - - } - - /** - * Test handle_page_header_wrappers() when there is an archived description - * - * @since 5.8.0 - * - * @return void - */ - public function test_handle_page_header_wrappers_title_and_desc() { - - $this->remove_header_actions(); - - // Output a description. - $handler = function( $desc ) { - return 'Archive description'; - }; - add_filter( 'llms_archive_description', $handler ); - - LLMS_Twenty_Twenty_Two::handle_page_header_wrappers(); - - $this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) ); - $this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) ); - $this->assertEquals( -1, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) ); - $this->assertEquals( 99999998, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) ); - - $this->remove_header_actions(); - - remove_filter( 'llms_archive_description', $handler ); - - } - - /** - * Test modify_columns_count() - * - * @since 5.8.0 - * - * @return void - */ - public function test_modify_columns_count() { - - $this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 1 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 2 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 3 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 999 ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty.php b/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty.php deleted file mode 100644 index 6436e95c73..0000000000 --- a/tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php -/** - * Test LLMS_Twenty_Twenty theme support class - * - * @package LifterLMS/Tests - * - * @group theme_support - * - * @since 3.37.0 - */ -class LLMS_Test_Twenty_Twenty extends LLMS_Unit_Test_Case { - - /** - * Setup the test case. - * - * @since 3.37.0 - * @since 4.3.0 Update theme support class instantiation. - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - - parent::set_up(); - update_option( 'template', 'twentytwenty' ); - $support = new LLMS_Theme_Support(); - $support->includes(); - - } - - /** - * Tear down the test case. - * - * @since 3.37.0 - * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes. - * - * @return void - */ - public function tear_down() { - - parent::tear_down(); - update_option( 'template', 'default' ); - - } - - /** - * Test the hide_meta_output() method. - * - * @since 3.37.0 - * - * @return void - */ - public function test_hide_meta_output() { - - $this->assertEquals( array( 'course', 'llms_membership', 'lesson', 'llms_quiz' ), LLMS_Twenty_Twenty::hide_meta_output( array() ) ); - $this->assertEquals( array( 'existing', 'course', 'llms_membership', 'lesson', 'llms_quiz' ), LLMS_Twenty_Twenty::hide_meta_output( array( 'existing' ) ) ); - - } - - /** - * Test is_page_full_width() method. - * - * @since 3.37.0 - * - * @return void - */ - public function test_is_page_full_width() { - - // Not Set. - $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) ); - - // Not full width. - update_post_meta( $page_id, '_wp_page_template', 'default' ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) ); - - // Is full width. - update_post_meta( $page_id, '_wp_page_template', 'templates/template-full-width.php' ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) ); - - } - - /** - * Default values for column counts will return 1 (default "thin" template) - * - * @since 3.37.0 - * - * @return [type] - */ - public function test_modify_columns_count_defaults() { - - $this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 1 ) ); - $this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 2 ) ); - - } - - /** - * Modify columns on catalogs. Returns 1 for default template and default column values for full width templates. - * - * @since 3.37.0 - * - * @return [type] - */ - public function test_modify_columns_count() { - - LLMS_Install::create_pages(); - LLMS_Install::create_visibilities(); - - foreach ( array( 'courses', 'memberships', 'checkout' ) as $page ) { - - $page_id = llms_get_page_id( $page ); - $url = get_permalink( $page_id ); - - $this->go_to( $url ); - $this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 2 ) ); - - update_post_meta( $page_id, '_wp_page_template', 'templates/template-full-width.php' ); - $this->assertEquals( 2, LLMS_Twenty_Twenty::modify_columns_count( 2 ) ); - - } - - } - -} diff --git a/tests/phpunit/unit-tests/traits/llms-test-trait-audio-video-embed.php b/tests/phpunit/unit-tests/traits/llms-test-trait-audio-video-embed.php deleted file mode 100644 index 5ec854c2ed..0000000000 --- a/tests/phpunit/unit-tests/traits/llms-test-trait-audio-video-embed.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -/** - * Tests for {@see LLMS_Trait_Audio_Video_Embed}. - * - * @group Traits - * @group LLMS_Post_Model - * - * @since 5.3.0 - */ -class LLMS_Test_Audio_Video_Embed_Trait extends LLMS_UnitTestCase { - - /** - * @var LLMS_Trait_Audio_Video_Embed - */ - protected $mock; - - /** - * Setup before running each test in this class. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - */ - public function set_up() { - - parent::set_up(); - - $args = array( - 'post_title' => 'Mock Post with the Audio Video Embed Trait', - ); - $this->mock = new class( 'new', $args ) extends LLMS_Post_Model { - - use LLMS_Trait_Audio_Video_Embed; - - protected $db_post_type = 'course'; # Limited to 20 characters. - protected $model_post_type = 'course'; # Limited to 20 characters. - - public function __construct( $model, $args = array() ) { - - $this->construct_audio_video_embed(); - parent::__construct( $model, $args ); - } - }; - } - - /** - * Test the {@see LLMS_Trait_Audio_Video_Embed::get_audio()} method. - * - * @since 5.3.0 - */ - public function test_get_audio() { - - $url = 'http://example.tld/audio_embed'; - $this->mock->set( 'audio_embed', $url ); - $expected = do_shortcode( sprintf( '[%1$s src="%2$s"]', 'audio', $url ) ); - $actual = $this->mock->get_audio(); - $this->assertEquals( $expected, $actual ); - } - - /** - * Test the {@see LLMS_Trait_Audio_Video_Embed::get_embed()} method. - * - * @since 5.3.0 - * @throws ReflectionException - */ - public function test_get_embed() { - - # Setup this test. - $audio_url = 'http://example.tld/audio_embed'; - $this->mock->set( 'audio_embed', $audio_url ); - $video_url = 'http://example.tld/video_embed'; - $this->mock->set( 'video_embed', $video_url ); - $expected_audio = wp_audio_shortcode( array( 'src' => $audio_url ) ); - $expected_video = wp_video_shortcode( array( 'src' => $video_url ) ); - - # Test all optional arguments. - $actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed' ); - $this->assertEquals( $expected_video, $actual ); - - # Test optional $prop argument. - $actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( 'type' => 'video' ) ); - $this->assertEquals( $expected_video, $actual ); - $actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( 'type' => 'audio' ) ); - $this->assertEquals( $expected_audio, $actual ); - - # Test with all arguments. - $actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( - 'type' => 'audio', - 'prop' => 'audio_embed', - ) ); - $this->assertEquals( $expected_audio, $actual ); - $actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( - 'type' => 'video', - 'prop' => 'video_embed', - ) ); - $this->assertEquals( $expected_video, $actual ); - } - - /** - * Test the {@see LLMS_Trait_Audio_Video_Embed::get_video()} method. - * - * @since 5.3.0 - */ - public function test_get_video() { - - $url = 'http://example.tld/video_embed'; - $this->mock->set( 'video_embed', $url ); - $expected = do_shortcode( sprintf( '[%1$s src="%2$s"]', 'video', $url ) ); - $actual = $this->mock->get_video(); - $this->assertEquals( $expected, $actual ); - } -} diff --git a/tests/phpunit/unit-tests/traits/llms-test-trait-sales-page.php b/tests/phpunit/unit-tests/traits/llms-test-trait-sales-page.php deleted file mode 100644 index 145e895c7a..0000000000 --- a/tests/phpunit/unit-tests/traits/llms-test-trait-sales-page.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Tests for {@see LLMS_Trait_Sales_Page}. - * - * @group Traits - * @group LLMS_Post_Model - * - * @since 5.3.0 - */ -class LLMS_Test_Sales_Page_Trait extends LLMS_UnitTestCase { - - /** - * @var LLMS_Trait_Sales_Page - */ - protected $mock; - - /** - * Setup before running each test in this class. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - */ - public function set_up() { - - parent::set_up(); - - $args = array( - 'post_title' => 'Mock Post with the Sales Page Trait', - ); - $this->mock = new class( 'new', $args ) extends LLMS_Post_Model { - - use LLMS_Trait_Sales_Page; - - protected $db_post_type = 'course'; - protected $model_post_type = 'course'; - - public function __construct( $model, $args = array() ) { - - $this->construct_sales_page(); - parent::__construct( $model, $args ); - } - }; - } - - /** - * Test the `construct_sales_page()` method. - * - * @since 5.3.0 - */ - public function test_construct_sales_page() { - /** - * {@see set_up()} should have created a mock object that called {@see LLMS_Trait_Sales_Page::construct_sales_page()}. - */ - $properties = $this->mock->get_properties(); - - $this->assertArrayHasKey( 'sales_page_content_page_id', $properties ); - $this->assertArrayHasKey( 'sales_page_content_type', $properties ); - $this->assertArrayHasKey( 'sales_page_content_url', $properties ); - } - - /** - * Test the `get_sales_page_url()` method. - * - * @since 5.3.0 - */ - public function test_get_sales_page_url() { - - # Test "Redirect to WordPress Page". - $page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) ); - $expected = get_permalink( $page_id ); - $this->mock->set( 'sales_page_content_type', 'page' ); - $this->mock->set( 'sales_page_content_page_id', $page_id ); - $actual = $this->mock->get_sales_page_url(); - $this->assertEquals( $expected, $actual ); - - # Test "Redirect to custom URL". - $expected = 'https://lifterlms.com'; - $this->mock->set( 'sales_page_content_type', 'url' ); - $this->mock->set( 'sales_page_content_url', $expected ); - $actual = $this->mock->get_sales_page_url(); - $this->assertEquals( $expected, $actual ); - - # Test "Display default course content". - $expected = get_permalink( $this->mock->get( 'id' ) ); - $this->mock->set( 'sales_page_content_type', 'none' ); - $actual = $this->mock->get_sales_page_url(); - $this->assertEquals( $expected, $actual ); - - # Test "Show custom content". - $expected = get_permalink( $this->mock->get( 'id' ) ); - $this->mock->set( 'sales_page_content_type', 'content' ); - $this->mock->set( 'excerpt', 'Please enroll in this course.' ); - $actual = $this->mock->get_sales_page_url(); - $this->assertEquals( $expected, $actual ); - } - - /** - * Test the `has_sales_page_redirect()` method. - * - * @since 5.3.0 - */ - public function test_has_sales_page_redirect() { - - # Test "Redirect to WordPress Page". - $page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) ); - $this->mock->set( 'sales_page_content_type', 'page' ); - $this->mock->set( 'sales_page_content_page_id', $page_id ); - $actual = $this->mock->has_sales_page_redirect(); - $this->assertTrue( $actual ); - - # Test "Redirect to custom URL". - $this->mock->set( 'sales_page_content_type', 'url' ); - $this->mock->set( 'sales_page_content_url', 'https://lifterlms.com' ); - $actual = $this->mock->has_sales_page_redirect(); - $this->assertTrue( $actual ); - - # Test "Display default course content". - $this->mock->set( 'sales_page_content_type', 'none' ); - $actual = $this->mock->has_sales_page_redirect(); - $this->assertFalse( $actual ); - - # Test "Show custom content". - $this->mock->set( 'sales_page_content_type', 'content' ); - $this->mock->set( 'excerpt', 'Please enroll in this course.' ); - $actual = $this->mock->has_sales_page_redirect(); - $this->assertFalse( $actual ); - } -} diff --git a/tests/phpunit/unit-tests/traits/llms-test-trait-singleton.php b/tests/phpunit/unit-tests/traits/llms-test-trait-singleton.php deleted file mode 100644 index ddff62e8ce..0000000000 --- a/tests/phpunit/unit-tests/traits/llms-test-trait-singleton.php +++ /dev/null @@ -1,141 +0,0 @@ -<?php -/** - * Tests for {@see LLMS_Trait_Singleton}. - * - * @group Traits - * - * @since 5.3.0 - */ -class LLMS_Test_Singleton_Trait extends LLMS_UnitTestCase { - - /** - * Dynamic class name of the mock class. - * - * Even though this property contains a string, it is documented as a class so that it can be used like this: - * `$this->mock_class::instance()` - * - * @since 5.3.0 - * - * @var LLMS_Trait_Singleton|object - */ - protected $mock_class; - - /** - * Setup before running each test in this class. - * - * @since 5.3.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @noinspection PhpHierarchyChecksInspection - */ - public function set_up() { - - parent::set_up(); - - # Instantiate an anonymous class that uses the trait to be tested. - $mock = new class { - - use LLMS_Trait_Singleton; - - protected $color; - - protected static $_instance = null; - - public static function deprecated_instance() { - if ( is_null( self::$_instance ) ) { - self::$_instance = new self(); - } - - return self::$_instance; - } - - public static function init() { - self::$_instance = null; - self::$instance = null; - } - - public function get_color() { - return $this->color; - } - - public function set_color( $color ) { - $this->color = $color; - } - }; - - $this->mock_class = get_class( $mock ); - } - - /** - * Test the {@see LLMS_Trait_Singleton::instance()} method where the exhibiting class has a - * deprecated `$_instance` property and may have it set by a 3rd party extended class. - * - * @since 5.3.0 - */ - public function test_deprecated_instance() { - - # Test where $_instance is not set. - $this->mock_class::init(); - $object = $this->mock_class::instance(); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $_instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, '_instance' ); - $this->assertEquals( $object, $instance_property ); - $this->assertEquals( $object, $_instance_property ); - - # Test where $_instance is set. - $this->mock_class::init(); - $object = $this->mock_class::deprecated_instance(); - $_instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, '_instance' ); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $this->assertEquals( $object, $_instance_property ); - $this->assertNull( $instance_property ); - - # Test setting $instance, then $_instance. - $this->mock_class::init(); - $object1 = $this->mock_class::instance(); - $object2 = $this->mock_class::deprecated_instance(); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $_instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, '_instance' ); - $this->assertEquals( $object1, $object2 ); - $this->assertEquals( $object1, $instance_property ); - $this->assertEquals( $object1, $_instance_property ); - - # Test setting $_instance, then $instance. - $this->mock_class::init(); - $object1 = $this->mock_class::deprecated_instance(); - $object2 = $this->mock_class::instance(); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $_instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, '_instance' ); - $this->assertEquals( $object1, $object2 ); - $this->assertEquals( $object1, $instance_property ); - $this->assertEquals( $object1, $_instance_property ); - } - - /** - * Test the {@see LLMS_Trait_Singleton::instance()} method. - * - * @since 5.3.0 - */ - public function test_instance() { - - # Test that the static instance property does not yet have an object. - $this->mock_class::init(); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $this->assertIsNotObject( $instance_property ); - - /** - * Test that {@see LLMS_Trait_Singleton::instance()} instantiates a new object, - * sets it in the static `$instance` property, and returns the new object. - */ - $object1 = $this->mock_class::instance(); - $instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' ); - $this->assertEquals( $object1, $instance_property ); - - # Test that 2 instances are the same. - $object1->set_color( 'red' ); - $object2 = $this->mock_class::instance(); - $object2->set_color( 'green' ); - $this->assertEquals( $object1, $object2 ); - $this->assertEquals( 'green', $object1->get_color() ); - } -} diff --git a/tests/phpunit/unit-tests/user/class-llms-test-abstract-student-data.php b/tests/phpunit/unit-tests/user/class-llms-test-abstract-student-data.php deleted file mode 100644 index 6eca6cbee7..0000000000 --- a/tests/phpunit/unit-tests/user/class-llms-test-abstract-student-data.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * Tests for LLMS_Abstract_User_Data - * @group LLMS_Student - * @since 3.9.0 - * @version 3.9.0 - */ -class LLMS_Test_Abstract_User_Data extends LLMS_UnitTestCase { - - /** - * Test exists function - * @return void - * @since 3.9.0 - * @version 3.9.0 - */ - public function text_exists() { - - $uid = $this->factory->user->create(); - - $student = new LLMS_Student( $uid ); - $this->assertTrue( $student->exists() ); - - $fake_student = new LLMS_Student( $uid + 1 ); - $this->assertFalse( $fake_student->exists() ); - - } - - /** - * test get_id method - * @return [type] [description] - * @since 3.9.0 - * @version 3.9.0 - */ - public function test_get_id() { - - $uid = $this->factory->user->create(); - $student = new LLMS_Student( $uid ); - $this->assertEquals( $uid, $student->get_id() ); - - } - - /** - * test get_user method - * @return [type] [description] - * @since 3.9.0 - * @version 3.9.0 - */ - public function test_get_user() { - - $uid = $this->factory->user->create(); - $student = new LLMS_Student( $uid ); - $this->assertTrue( is_a( $student->get_user(), 'WP_User' ) ); - - } - - /** - * Test Student Getters and Setters - * @return void - * @since 3.9.0 - * @version 3.9.0 - */ - public function test_getters_setters() { - - $uid = $this->factory->user->create( array( 'role' => 'student' ) ); - $user = new WP_User( $uid ); - $student = new LLMS_Student( $uid ); - - // test some core prefixed stuff from the usermeta table - $student->set( 'first_name', 'Student' ); - $student->set( 'last_name', 'McStudentFace' ); - $this->assertEquals( get_user_meta( $uid, 'first_name', true ), $student->get( 'first_name' ) ); - $this->assertEquals( get_user_meta( $uid, 'last_name', true ), $student->get( 'last_name' ) ); - - // stuff from the user table - $this->assertEquals( $user->user_email, $student->get( 'user_email' ) ); - - // llms custom user meta - $student->set( 'billing_address', '123 Student Place' ); - $this->assertEquals( get_user_meta( $uid, 'llms_billing_address', true ), $student->get( 'billing_address' ) ); - - // don't prefix - $student->set( 'this_is_third_party', '123456', false ); - add_filter( 'llms_student_unprefixed_metas', function( $metas ) { - $metas[] = 'this_is_third_party'; - return $metas; - } ); - $this->assertEquals( get_user_meta( $uid, 'this_is_third_party', true ), $student->get( 'this_is_third_party' ) ); - - } - - - - -} diff --git a/tests/phpunit/unit-tests/user/class-llms-test-person-handler.php b/tests/phpunit/unit-tests/user/class-llms-test-person-handler.php deleted file mode 100644 index cb6780d6d5..0000000000 --- a/tests/phpunit/unit-tests/user/class-llms-test-person-handler.php +++ /dev/null @@ -1,635 +0,0 @@ -<?php -/** - * Tests for LifterLMS Core Functions - * - * @group LLMS_Student - * @group LLMS_Person_Handler - * - * @since 3.19.4 - * @since 3.29.4 Unknown. - * @since 3.37.17 Add voucher-related tests. - * @since 4.5.0 Added tests on account.signon event recorded on user registration. - * @since 5.0.0 Update to work with changes from LLMS_Forms. - * Add tests for the LLMS_Person_Handler::get_login_forms() method. - * Login tests don't rely on deprecated option `lifterlms_registration_generate_username`. - * 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 - */ -class LLMS_Test_Person_Handler extends LLMS_UnitTestCase { - - /** - * Test username generation - * @return void - * @since 3.19.4 - * @version 3.19.4 - */ - public function test_generate_username() { - - // username is first part of email - $this->assertEquals( 'mock', LLMS_Person_Handler::generate_username( 'mock@whatever.com' ) ); - - // create a user with the mock username - $this->factory->user->create( array( - 'user_login' => 'mock', - ) ); - - // test that usernames are unique - $i = 1; - while ( $i <= 5 ) { - $this->factory->user->create( array( - 'user_login' => sprintf( 'mock%d', $i ), - ) ); - $this->assertEquals( sprintf( 'mock%d', $i+1 ), LLMS_Person_Handler::generate_username( 'mock@whatever.com' ) ); - $i++; - } - - // test character sanitization - $tests = array( - 'mock_mock' => 'mock_mock', - "mock'mock" => "mockmock", - 'mock+mock' => "mockmock", - 'mock.mock' => "mock.mock", - 'mock-mock' => "mock-mock", - 'mock mock' => "mock mock", - 'mock!mock' => "mockmock", - ); - - foreach ( $tests as $email => $expect) { - $this->assertEquals( $expect, LLMS_Person_Handler::generate_username( $email . '@whatever.com' ) ); - } - - } - - - // public function test_get_available_fields() {} - - /** - * Test the get_login_fields() method. - * - * It should return an array of LifterLMS Form Fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_login_fields() { - - $fields = LLMS_Person_Handler::get_login_fields(); - - $this->assertTrue( is_array( $fields ) ); - $this->assertEquals( 5, count( $fields ) ); - foreach ( $fields as $field ) { - $this->assertTrue( is_array( $field ) ); - $this->assertArrayHasKey( 'id', $field ); - } - - } - - /** - * Test the get_login_fields() method when the layout is columns - * - * It should return an array of LifterLMS Form Fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_login_fields_layout_columns() { - - $default = LLMS_Person_Handler::get_login_fields(); - $fields = LLMS_Person_Handler::get_login_fields( 'columns' ); - - // Default value is "columns". - $this->assertEquals( $default, $fields ); - - $this->assertEquals( 6, $fields[0]['columns'] ); - $this->assertEquals( 6, $fields[1]['columns'] ); - $this->assertEquals( 3, $fields[2]['columns'] ); - $this->assertEquals( 6, $fields[3]['columns'] ); - $this->assertEquals( 3, $fields[4]['columns'] ); - - } - - /** - * Test the get_login_fields() method when the layout is stacked - * - * It should return an array of LifterLMS Form Fields. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_login_fields_layout_stacked() { - - $fields = LLMS_Person_Handler::get_login_fields( 'stacked' ); - - $this->assertEquals( 12, $fields[0]['columns'] ); - $this->assertEquals( 12, $fields[1]['columns'] ); - $this->assertEquals( 12, $fields[2]['columns'] ); - $this->assertEquals( 6, $fields[3]['columns'] ); - $this->assertEquals( 6, $fields[4]['columns'] ); - - } - - /** - * Test get_login_fields() when usernames are enabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_login_fields_usernames_enabled() { - - add_filter( 'llms_are_usernames_enabled', '__return_true' ); - $field = LLMS_Person_Handler::get_login_fields()[0]; - $this->assertEquals( 'Username or Email Address', $field['label'] ); - $this->assertEquals( 'text', $field['type'] ); - remove_filter( 'llms_are_usernames_enabled', '__return_true' ); - - } - - /** - * Test get_login_fields() when usernames are disabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_login_fields_usernames_disabled() { - - add_filter( 'llms_are_usernames_enabled', '__return_false' ); - $field = LLMS_Person_Handler::get_login_fields()[0]; - $this->assertEquals( 'Email Address', $field['label'] ); - $this->assertEquals( 'email', $field['type'] ); - remove_filter( 'llms_are_usernames_enabled', '__return_false' ); - - } - - /** - * Test get_lost_password_fields() when usernames are enabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_lost_password_fields_usernames_enabled() { - - add_filter( 'llms_are_usernames_enabled', '__return_true' ); - $fields = LLMS_Person_Handler::get_lost_password_fields(); - $this->assertTrue( false !== strpos( $fields[0]['value'], 'username' ) ); - $this->assertEquals( 'Username or Email Address', $fields[1]['label'] ); - $this->assertEquals( 'text', $fields[1]['type'] ); - remove_filter( 'llms_are_usernames_enabled', '__return_true' ); - - } - - /** - * Test get_lost_password_fields() when usernames are disabled. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_lost_password_fields_usernames_disabled() { - - add_filter( 'llms_are_usernames_enabled', '__return_false' ); - $fields = LLMS_Person_Handler::get_lost_password_fields(); - $this->assertFalse( strpos( $fields[0]['value'], 'username' ) ); - $this->assertEquals( 'Email Address', $fields[1]['label'] ); - $this->assertEquals( 'email', $fields[1]['type'] ); - remove_filter( 'llms_are_usernames_enabled', '__return_false' ); - - } - - /** - * Test get_password_reset_fields() when "custom" password reset fields exist on the checkout form. - * - * @since 5.0.0 - * - * @return void - */ - public function test_get_password_reset_fields_from_checkout() { - - update_option( 'lifterlms_registration_password_strength', 'yes' ); - LLMS_Forms::instance()->create( 'checkout', true ); - - add_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) { - $this->assertEquals( 'checkout', $location ); - return $fields; - }, 10, 4 ); - - $expect = array( - 'password', - 'password_confirm', - 'llms-password-strength-meter', - 'llms_lost_password_button', - 'llms_reset_key', - 'llms_reset_login', - ); - $this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) ); - - } - - /** - * Test get_password_reset_fields() when "custom" password reset fields don't exist on checkout but do exist on reg form. - * - * @since 5.0.0 - * @since 5.1.0 Made sure no users are logged in before retrieving password reset fields. - * And avoid auto-adding of required fields like password/email when retrieving form's fields. - * - * @return void - */ - public function test_get_password_reset_fields_from_registration() { - - wp_update_post( array( - 'ID' => LLMS_Forms::instance()->create( 'checkout', true ), - 'post_content' => '', - ) ); - - LLMS_Forms::instance()->create( 'registration', true ); - // Avoid auto adding of fields like password/email. - add_filter( 'llms_forms_required_block_fields', '__return_empty_array' ); - add_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) { - $this->assertEquals( 'registration', $location ); - return $fields; - }, 10, 4 ); - - // Log out. - wp_set_current_user( null ); - - $expect = array( - 'password', - 'password_confirm', - 'llms-password-strength-meter', - 'llms_lost_password_button', - 'llms_reset_key', - 'llms_reset_login', - ); - $this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) ); - - remove_filter( 'llms_forms_required_block_fields', '__return_empty_array' ); - - } - - /** - * Test get_password_reset_fields() when "custom" password reset fields don't exist on checkout but do exist on reg form. - * - * @since 5.0.0 - * @since 5.1.0 Avoid auto-adding of required fields like password/email when retrieving form's fields. - * - * @return void - */ - public function test_get_password_reset_fields_from_fallback() { - - // Avoid auto adding of fields like password/email. - add_filter( 'llms_forms_required_block_fields', '__return_empty_array' ); - add_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) { - $this->assertEquals( 'fallback', $location ); - return $fields; - }, 10, 4 ); - - $expect = array( - 'password', - 'password_confirm', - 'llms-password-strength-meter', - 'llms_lost_password_button', - 'llms_reset_key', - 'llms_reset_login', - ); - $this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) ); - - remove_filter( 'llms_forms_required_block_fields', '__return_empty_array' ); - - } - - /** - * Test logging in with a username. - * - * @since 3.29.4 - * @since 5.0.0 Remove deprecated option `lifterlms_registration_generate_username` and allow username login via filter. - * - * @return void - */ - public function test_login_with_username() { - - // Enable Usernames. - add_filter( 'llms_are_usernames_enabled', '__return_true' ); - - // Missing login. - $login = LLMS_Person_Handler::login( array( - 'llms_password' => 'faker', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'llms_login', $login ); - - // Missing Password - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'faker', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'llms_password', $login ); - - // Totally Invalid creds. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => '3OGgpZZ146cH3vw775aMg1R7qQIrF4ph', - 'llms_password' => 'Ip439RKmf0am5MWRjD38ov6M45OEYs79', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'login-error', $login ); - - // Test against a real user with bad creds. - $user = $this->factory->user->create_and_get( array( 'user_login' => 'test_user_login', 'user_pass' => '1234' ) ); - $uid = $user->ID; - - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'test_user_login', - 'llms_password' => '1', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'login-error', $login ); - - // Success. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'test_user_login', - 'llms_password' => '1234', - ) ); - - $this->assertEquals( $uid, $login ); - wp_logout(); - - - // Use a fake email address in the login field. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'fake@whatever.com', - 'llms_password' => '1234', - ) ); - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'login-error', $login ); - - // Use the real email address in the login field. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => $user->user_email, - 'llms_password' => '1234', - ) ); - $this->assertEquals( $uid, $login ); - wp_logout(); - - remove_filter( 'llms_are_usernames_enabled', '__return_true' ); - - } - - /** - * Test logging in with a username. - * - * @since 3.29.4 - * @since 5.0.0 Remove deprecated option `lifterlms_registration_generate_username`. - * - * @return void - */ - public function test_login_with_email() { - - // Missing login. - $login = LLMS_Person_Handler::login( array( - 'llms_password' => 'faker', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'llms_login', $login ); - - // Invalid email address. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'faker', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'llms_login', $login ); - - // Missing password. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => 'faker@fake.tld', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'llms_password', $login ); - - // Totally Invalid creds. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => '3OGgpZZ146cH3vw775aMg1R7qQIrF4ph@fake.tld', - 'llms_password' => 'Ip439RKmf0am5MWRjD38ov6M45OEYs79', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'login-error', $login ); - - // Test against a real user with bad creds. - $user = $this->factory->user->create_and_get( array( 'user_pass' => '1234' ) ); - - $login = LLMS_Person_Handler::login( array( - 'llms_login' => $user->user_email, - 'llms_password' => '1', - ) ); - - $this->assertIsWPError( $login ); - $this->assertWPErrorCodeEquals( 'login-error', $login ); - - // Success. - $login = LLMS_Person_Handler::login( array( - 'llms_login' => $user->user_email, - 'llms_password' => '1234', - ) ); - - $this->assertEquals( $user->ID, $login ); - wp_logout(); - - // Make sure that email addresses with an apostrophe in them can login without issue. - $user = $this->factory->user->create_and_get( array( 'user_email' => "mock\'mock@what.org", 'user_pass' => '1234' ) ); - $login = LLMS_Person_Handler::login( array( - 'llms_login' => $user->user_email, - 'llms_password' => '1234', - ) ); - - $this->assertEquals( $user->ID, $login ); - wp_logout(); - - } - - /** - * Test account.signon event recorded on user registration - * - * @since 4.5.0 - * @since 5.0.0 Add email confirm fields to reflect new form defaults. - */ - public function test_account_signon_event_recorded_on_registration_signon() { - - LLMS_Install::create_pages(); - LLMS_Forms::instance()->install( true ); - - global $wpdb; - - $data = $this->get_mock_registration_data(); - $data['email_address'] = "new_{$data['email_address']}"; - $data['email_address_confirm'] = $data['email_address']; - - $query_signon_event = " - SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events - WHERE event_type='account' - AND event_action='signon' - AND object_type='user' - AND actor_id='%d' - "; - - // Test no event registered, if no signon. - $user_id = llms_register_user( $data, $screen = 'registration', false ); - $this->assertEquals( 0, $wpdb->get_var( $wpdb->prepare( $query_signon_event, $user_id ) ) ); - - // Test event registered when signing on registration (defaults). - $data['email_address'] = "new1_{$data['email_address']}"; - $data['email_address_confirm'] = $data['email_address']; - $user_id = llms_register_user( $data ); - $this->assertEquals( 1, $wpdb->get_var( $wpdb->prepare( $query_signon_event, $user_id ) ) ); - - // Clean up tables. - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events" ); - $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions" ); - } - - /** - * Test the deprecated update() method. - * - * This test remains to ensure backwards compatibility. - * - * @since 3.26.1 - * @since 5.0.0 Create forms before running & update error codes to match updated codes. - * - * @expectedDeprecated LLMS_Person_Handler::update() - * - * @return void - */ - public function test_update() { - - LLMS_Install::create_pages(); - LLMS_Forms::instance()->install(); - - $data = array(); - - // No user Id supplied. - $update = LLMS_Person_Handler::update( $data, 'account' ); - $this->assertTrue( is_wp_error( $update ) ); - $this->assertEquals( 'llms-form-no-user', $update->get_error_code() ); - - $uid = $this->factory->user->create( array( 'role' => 'student' ) ); - $user = new WP_User( $uid ); - - // user Id Interpreted from current logged in user. - wp_set_current_user( $uid ); - $update = LLMS_Person_Handler::update( $data, 'account' ); - $this->assertTrue( is_wp_error( $update ) ); - $this->assertFalse( in_array( 'llms-form-no-user', $update->get_error_codes(), true ) ); - wp_set_current_user( null ); - - // Used ID explicitly passed. - $data['user_id'] = $uid; - $update = LLMS_Person_Handler::update( $data, 'account' ); - $this->assertTrue( is_wp_error( $update ) ); - $this->assertTrue( in_array( 'llms-form-no-user', $update->get_error_codes(), true ) ); - - } - - private function get_mock_registration_data( $data = array() ) { - - $password = wp_generate_password(); - - return wp_parse_args( $data, array( - 'user_login' => 'mocker', - 'email_address' => 'mocker@mock.com', - 'first_name' => 'Bird', - 'last_name' => 'Person', - 'llms_billing_address_1' => '1234 Street Ave.', - 'llms_billing_address_2' => '#567', - 'llms_billing_city' => 'Anywhere,', - 'llms_billing_state' => 'CA', - 'llms_billing_zip' => '12345', - 'llms_billing_country' => 'US', - 'llms_agree_to_terms' => 'yes', - 'password' => $password, - 'password_confirm' => $password, - ) ); - - } - - /** - * test validate fields - * - * @since Unknown - * - * @expectedDeprecated LLMS_Person_Handler::validate_fields() - * @expectedDeprecated LLMS_Person_Handler::get_available_fields() - * - * @return void - */ - public function test_validate_fields() { - - LLMS_Forms::instance()->install(); - /** - * Registration - */ - - // no data - $this->assertTrue( is_wp_error( LLMS_Person_Handler::validate_fields( array(), 'registration' ) ) ); - - $data = $this->get_mock_registration_data(); - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'registration' ) ); - - // check emails with quotes - $data['email_address'] = "mock'mock@what.org"; - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'registration' ) ); - - - /** - * Login - */ - - // no data - $this->assertTrue( is_wp_error( LLMS_Person_Handler::validate_fields( array(), 'login' ) ) ); - - $data = array( - 'llms_login' => 'mocker@mock.com', - 'llms_password' => '4bKyvI41Xxnf', - ); - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'login' ) ); - - // check emails with quotes - $data = array( - 'llms_login' => "moc'ker@mock.com", - 'llms_password' => '4bKyvI41Xxnf', - ); - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'login' ) ); - - /** - * Update - */ - - // no data - $this->assertTrue( is_wp_error( LLMS_Person_Handler::validate_fields( array(), 'account' ) ) ); - - $data = $this->get_mock_registration_data(); - $data['email_address_confirm'] = $data['email_address']; - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'account' ) ); - - - $uid = $this->factory->user->create( array( - 'user_email' =>"mock'mock@what.org", - ) ); - wp_set_current_user( $uid ); - - $data = $this->get_mock_registration_data(); - $data['email_address'] = "mock'mock@what.org"; - $data['email_address_confirm'] = $data['email_address']; - $this->assertTrue( LLMS_Person_Handler::validate_fields( $data, 'account' ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/user/class-llms-test-student-quizzes.php b/tests/phpunit/unit-tests/user/class-llms-test-student-quizzes.php deleted file mode 100644 index 3723973763..0000000000 --- a/tests/phpunit/unit-tests/user/class-llms-test-student-quizzes.php +++ /dev/null @@ -1,171 +0,0 @@ -<?php -/** - * Tests for LifterLMS Student Functions - * - * @group quizzes - * @group student_quizzes - * @group LLMS_Student - * - * @since 3.9.0 - */ -class LLMS_Test_Student_Quizzes extends LLMS_UnitTestCase { - - /** - * Assert that two quiz attempts are deeply equal - * - * @since 4.21.2 - * - * @param LLMS_Quiz_Attempt $expected Expected attempt object. - * @param LLMS_Quiz_Attempt $actual Actual attempt object. - * @return void - */ - private function assertAttemptsAreEqual( $expected, $actual ) { - - $props = array( - 'id', - 'student_id', - 'quiz_id', - 'lesson_id', - 'start_date', - 'update_date', - 'end_date', - 'status', - 'attempt', - 'grade', - ); - - foreach ( $props as $prop ) { - $this->assertEquals( $expected->get( $prop ), $actual->get( $prop ), $prop ); - } - - } - - /** - * Create a student with sample quizzes. - * - * @since Unknown - * - * @return LLMS_Student - */ - private function get_student_with_quizzes( $attempts = 3 ) { - - $uid = $this->factory->user->create(); - $student = llms_get_student( $uid ); - $courses = $this->generate_mock_courses( $attempts, 1, 1, 1 ); - $this->complete_courses_for_student( $uid, $courses ); - return $student; - - } - - /** - * Retrieve a quiz attempt for a given student. - * - * @since 4.21.2 - * - * @param LLMS_Student $student Student object. - * @return LLMS_Quiz_Attempt - */ - private function get_attempt( $student ) { - - $course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 1 )[0] ); - $lesson = $course->get_lessons()[0]; - $quiz = $lesson->get_quiz(); - - $attempt = LLMS_Quiz_Attempt::init( $quiz->get( 'id' ), $lesson->get( 'id' ), absint( $student->get( 'id' ) ) ); - - $attempt->save(); - - return new LLMS_Quiz_Attempt( $attempt->get( 'id' ) ); - - } - - /** - * Test delete_attempt() - * - * @since 3.9.0 - * @since 3.16.11 Unknown. - * @since 4.21.2 Only users who can view_grades can delete attempts. - * - * @return void - */ - public function test_delete_attempt() { - - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - - $i = 1; - while ( $i <= 5 ) { - - $student = $this->get_student_with_quizzes(); - $attempts = $student->quizzes()->get_all(); - $id = rand( 0, count( $attempts ) - 1 ); - $attempt = $attempts[ $id ]; - - $this->assertTrue( $student->quizzes()->delete_attempt( $attempt->get( 'id' ) ) ); - $this->assertFalse( $attempt->exists() ); - - $i++; - - } - - } - - /** - * Test get_all() - * - * @since 4.21.2 - * - * @return void - */ - public function test_get_all() { - - $student = $this->get_student_with_quizzes( 10 ); - - $attempts = $student->quizzes()->get_all(); - - foreach ( $attempts as $attempt ) { - - $this->assertTrue( $attempt instanceof LLMS_Quiz_Attempt ); - $this->assertEquals( $student->get( 'id' ), absint( $attempt->get( 'student_id' ) ) ); - - } - - } - - /** - * Test get_attempt_by_id() and get_attempt_by_key() - * - * @since 4.21.2 - * @since 5.9.0 Don't use an invalid fake hash length. - * - * @return void - */ - public function test_attempt_getters() { - - $student = llms_get_student( $this->factory->user->create() ); - $attempt = $this->get_attempt( $student ); - - wp_set_current_user( $student->get( 'id' ) ); - - // Get by ID. - $this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) ); - - // Get by Key. - $this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_key( $attempt->get_key() ) ); - - // ID Doesn't exit. - $this->assertFalse( $student->quizzes()->get_attempt_by_id( absint( $attempt->get( 'id' ) ) + 1 ) ); - - // Key doesn't exist. - $this->assertFalse( $student->quizzes()->get_attempt_by_key( 'FAKE' ) ); - - // ID exists but Wrong student. - wp_set_current_user( $this->factory->user->create() ); - $this->assertFalse( $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) ); - - // Admin can view. - wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); - $this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/user/class-llms-test-student.php b/tests/phpunit/unit-tests/user/class-llms-test-student.php deleted file mode 100644 index 3a28f631bd..0000000000 --- a/tests/phpunit/unit-tests/user/class-llms-test-student.php +++ /dev/null @@ -1,645 +0,0 @@ -<?php -/** - * Tests for LifterLMS Student Functions - * @group LLMS_Student - * @since 3.5.0 - * @since 3.33.0 Add delete enrollment tests. - * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion. - * @since 5.2.0 Added tests on `get_registration_date`. - */ -class LLMS_Test_Student extends LLMS_UnitTestCase { - - /** - * Test mark_complete() and mark_incomplete() on a tracks, courses, sections, and lessons - * - * @since 3.5.0 - * @since 3.17.0 - * - * @return void - */ - public function test_completion_incompletion() { - - $courses = $this->generate_mock_courses( 3, 3, 3, 0 ); - $student = $this->factory->user->create( array( 'role' => 'student' ) ); - $track = wp_insert_term( 'test track', 'course_track' ); - - // nothing completed - foreach ( $courses as $c_i => $cid ) { - - wp_set_object_terms( $cid, array( $track['term_id'] ), 'course_track', false ); - - $this->assertFalse( llms_is_complete( $student, $cid, 'course' ) ); - - // check sections - $course = llms_get_post( $cid ); - foreach ( $course->get_sections( 'ids' ) as $s_i => $sid ) { - - // no data recorded - $this->assertFalse( llms_is_complete( $student, $sid, 'section' ) ); - - // check lessons - $section = llms_get_post( $sid ); - foreach ( $section->get_lessons( 'ids' ) as $l_i => $lid ) { - - // no data recorded (incomplete) - $this->assertFalse( llms_is_complete( $student, $sid, 'lesson' ) ); - - // marked completed - llms_mark_complete( $student, $lid, 'lesson' ); - $this->assertTrue( llms_is_complete( $student, $lid, 'lesson' ) ); - - // marked incomplete - llms_mark_incomplete( $student, $lid, 'lesson' ); - $this->assertFalse( llms_is_complete( $student, $sid, 'lesson' ) ); - - // complete it again to check parents - llms_mark_complete( $student, $lid, 'lesson' ); - $this->assertTrue( llms_is_complete( $student, $lid, 'lesson' ) ); - - // parent should still be incomplete - if ( $l_i <= 1 ) { - $this->assertFalse( llms_is_complete( $student, $sid, 'section' ) ); - } - - } - - // all lessons complete - $this->assertTrue( llms_is_complete( $student, $sid, 'section' ) ); - - // mark last lesson as incomplete - llms_mark_incomplete( $student, $lid, 'lesson' ); - $this->assertFalse( llms_is_complete( $student, $sid, 'section' ) ); - - // mark complete again for parent checks - llms_mark_complete( $student, $lid, 'lesson' ); - $this->assertTrue( llms_is_complete( $student, $sid, 'section' ) ); - - // parent should still be incomplete - if ( $s_i <= 1 ) { - $this->assertFalse( llms_is_complete( $student, $cid, 'course' ) ); - $this->assertFalse( llms_is_complete( $student, $track['term_id'], 'course_track' ) ); - } - - } - - $this->assertTrue( llms_is_complete( $student, $cid, 'course' ) ); - $this->assertTrue( llms_is_complete( $student, $track['term_id'], 'course_track' ) ); - - // mark last lesson as incomplete - llms_mark_incomplete( $student, $lid, 'lesson' ); - $this->assertFalse( llms_is_complete( $student, $cid, 'course' ) ); - $this->assertFalse( llms_is_complete( $student, $track['term_id'], 'course_track' ) ); - - // mark complete again for parents - llms_mark_complete( $student, $lid, 'lesson' ); - $this->assertTrue( llms_is_complete( $student, $cid, 'course' ) ); - $this->assertTrue( llms_is_complete( $student, $track['term_id'], 'course_track' ) ); - - } - - } - - /** - * Test whether a user is_enrolled() in a course or membership - * @return void - * @since 3.5.0 - * @version 3.28.0 - */ - public function test_enrollment() { - - // Create new user - $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); - - // Create new course - $course_id = $this->factory->post->create( array( 'post_type' => 'course' ) ); - - // Create new membership - $memb_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ); - - // Student shouldn't be enrolled in newly created course/membership - $this->assertFalse( llms_is_user_enrolled( $user_id, $course_id ) ); - $this->assertFalse( llms_is_user_enrolled( $user_id, $memb_id ) ); - - // Enroll Student in newly created course/membership - llms_enroll_student( $user_id, $course_id, 'test_is_enrolled' ); - llms_enroll_student( $user_id, $memb_id, 'test_is_enrolled' ); - - // Student should be enrolled in course/membership - $this->assertTrue( llms_is_user_enrolled( $user_id, $course_id ) ); - $this->assertTrue( llms_is_user_enrolled( $user_id, $memb_id ) ); - - // Wait 1 second before unenrolling Student - // otherwise, enrollment and unenrollment postmeta will have identical timestamps - sleep( 1 ); - - // Unenroll Student in newly created course/membership - llms_unenroll_student( $user_id, $course_id, 'cancelled', 'test_is_enrolled' ); - llms_unenroll_student( $user_id, $memb_id, 'cancelled', 'test_is_enrolled' ); - - // Student should be not enrolled in newly created course/membership - $this->assertFalse( llms_is_user_enrolled( $user_id, $course_id ) ); - $this->assertFalse( llms_is_user_enrolled( $user_id, $memb_id ) ); - - // these were tests against now deprecated has_access - sleep( 1 ); - - $student = $this->get_mock_student(); - - $course_id = $this->generate_mock_courses()[0]; - - // no access - $this->assertFalse( $student->is_enrolled( $course_id ) ); - - // has access - llms_enroll_student( $student->get_id(), $course_id ); - $this->assertTrue( $student->is_enrolled( $course_id ) ); - - // check access after an access plan has expired access - $gateway = LLMS()->payment_gateways()->get_gateway_by_id( 'manual' ); - update_option( $gateway->get_option_name( 'enabled' ), 'yes' ); - - // new student - $student = $this->get_mock_student(); - - // create an access plan - $plan = new LLMS_Access_Plan( 'new', 'Test Access Plan' ); - $plan_data = array( - 'access_expiration' => 'limited-period', - 'access_length' => '1', - 'access_period' => 'month', - 'frequency' => 25, - 'is_free' => 'no', - 'length' => 0, - 'on_sale' => 'no', - 'period' => 'day', - 'price' => 25.00, - 'product_id' => $course_id, - 'sku' => 'accessplansku', - 'trial_offer' => 'no', - ); - foreach ( $plan_data as $key => $val ) { - $plan->set( $key, $val ); - } - - $order = new LLMS_Order( 'new' ); - $order->init( $student, $plan, $gateway ); - - $order->set( 'status', 'llms-completed' ); - update_option( $gateway->get_option_name( 'enabled' ), 'no' ); // prevent potential issues elsewhere - - // should be enrolled with no issues - $this->assertTrue( $student->is_enrolled( $course_id ) ); - - // fast forward - llms_mock_current_time( date( 'Y-m-d', current_time( 'timestamp' ) + YEAR_IN_SECONDS ) ); - - sleep( 1 ); // so the expiration status is later than the enrollment - - // trigger expiration - do_action( 'llms_access_plan_expiration', $order->get( 'id' ) ); - - $this->assertFalse( $student->is_enrolled( $course_id ) ); - - sleep( 1 ); - - // manually re-enroll the student, admin enrollment should take precedence here even though they no longer have access - llms_enroll_student( $student->get_id(), $course_id ); - $this->assertTrue( $student->is_enrolled( $course_id ) ); - - } - - /** - * Test get_enrollment_date() - * - * @since 3.17.0 - * @since 3.33.0 Add test after enrollment deletion. - * - * @return void - */ - public function test_get_enrollment_date() { - - $courses = $this->generate_mock_courses( 3, 0, 0, 0 ); - $student = $this->get_mock_student(); - - $now = time(); - $format = 'Y-m-d H:i:s'; - - // nothing completed - foreach ( $courses as $cid ) { - - $ts = $now + ( DAY_IN_SECONDS * rand( 1, 50 ) ); - $date = date( $format, $ts ); - - llms_mock_current_time( $date ); - - // enrollment date should match currently mocked date - $student->enroll( $cid ); - $this->assertEquals( $date, $student->get_enrollment_date( $cid, 'enrolled', $format ) ); - - $ts += HOUR_IN_SECONDS; - $new_date = date( $format, $ts ); - llms_mock_current_time( $new_date ); - - // updated date should be an hour later - $student->unenroll( $cid ); - $this->assertEquals( $new_date, $student->get_enrollment_date( $cid, 'updated', $format ) ); - - // enrollment date should still be the original date - $this->assertEquals( $date, $student->get_enrollment_date( $cid, 'enrolled', $format ) ); - - // after enrollment deletion there should be no 'updated' or 'enrolled' date - $student->delete_enrollment( $cid ); - $this->assertFalse( $student->get_enrollment_date( $cid, 'updated', $format ) ); - $this->assertFalse( $student->get_enrollment_date( $cid, 'enrolled', $format ) ); - - } - - } - - /** - * Test Student Getters and Setters - * @return void - * @since 3.5.1 - * @version 3.5.1 - */ - public function test_getters_setters() { - - $uid = $this->factory->user->create( array( 'role' => 'student' ) ); - $user = new WP_User( $uid ); - $student = new LLMS_Student( $uid ); - - // test some core prefixed stuff from the usermeta table - $student->set( 'first_name', 'Student' ); - $student->set( 'last_name', 'McStudentFace' ); - $this->assertEquals( get_user_meta( $uid, 'first_name', true ), $student->get( 'first_name' ) ); - $this->assertEquals( get_user_meta( $uid, 'last_name', true ), $student->get( 'last_name' ) ); - - // stuff from the user table - $this->assertEquals( $user->user_email, $student->get( 'user_email' ) ); - - // llms custom user meta - $student->set( 'billing_address', '123 Student Place' ); - $this->assertEquals( get_user_meta( $uid, 'llms_billing_address', true ), $student->get( 'billing_address' ) ); - - } - - /** - * Test get_name() function - * @return void - * @since 3.5.1 - * @version 3.5.1 - */ - public function test_get_name() { - - $uid = $this->factory->user->create( array( - 'role' => 'student' - ) ); - $user = new WP_User( $uid ); - $student = new LLMS_Student( $uid ); - - // no first/last name set, should return display name - $this->assertEquals( $user->display_name, $student->get_name() ); - - // set a first & last name - $uid = $this->factory->user->create( array( - 'first_name' => 'Student', - 'last_name' => 'McStudentFace', - 'role' => 'student' - ) ); - $student = new LLMS_Student( $uid ); - $this->assertEquals( 'Student McStudentFace', $student->get_name() ); - - } - - /** - * Test get_enrollment_status() - * - * @since 3.17.0 - * @since 3.33.0 Add test after enrollment deletion. - * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion. - * @since 4.18.0 Removed the sleep delay between status changes to test statuses with the same date & time. - * - * @return void - */ - public function test_get_enrollment_status() { - - $course_id = $this->generate_mock_courses( 1, 1, 1, 0 )[0]; - $course = llms_get_post( $course_id ); - $student = llms_get_student( $this->factory->user->create( array( 'role' => 'student' ) ) ); - - // no status - $this->assertFalse( $student->get_enrollment_status( $course_id ) ); - - // enrolled - $student->enroll( $course_id ); - $this->assertEquals( 'enrolled', $student->get_enrollment_status( $course_id ) ); - $this->assertEquals( 'enrolled', $student->get_enrollment_status( $course_id, false ) ); - // check from a lesson - $this->assertEquals( 'enrolled', $student->get_enrollment_status( $course->get_lessons( 'ids' )[0] ) ); - $this->assertEquals( 'enrolled', $student->get_enrollment_status( $course->get_lessons( 'ids' )[0] ), false ); - - // expired - $student->unenroll( $course_id ); - $this->assertEquals( 'expired', $student->get_enrollment_status( $course_id ) ); - $this->assertEquals( 'expired', $student->get_enrollment_status( $course_id, false ) ); - - // deleted - $student->delete_enrollment( $course_id ); - $this->assertFalse( $student->get_enrollment_status( $course_id ) ); - $this->assertFalse( $student->get_enrollment_status( $course_id, false ) ); - - // Test auto-enrollments deletion. - // create a membership. - $membership = new LLMS_Membership( 'new', 'Membership Title' ); - $membership_id = $membership->get('id'); - - // set the courses as membership auto-enrollments. - $courses = $this->generate_mock_courses( 2, 0, 0, 0 ); - $membership->set( 'auto_enroll', $courses ); - - $student->enroll( $membership_id ); - $student->delete_enrollment( $membership_id ); - $this->assertFalse( $student->get_enrollment_status( $membership_id ) ); - $this->assertFalse( $student->get_enrollment_status( $membership_id, false ) ); - $this->assertFalse( $student->get_enrollment_status( $courses[0] ) ); - $this->assertFalse( $student->get_enrollment_status( $courses[0], false ) ); - $this->assertFalse( $student->get_enrollment_status( $courses[1] ) ); - $this->assertFalse( $student->get_enrollment_status( $courses[1], false ) ); - - } - - /** - * Test get_grade() method - * @return void - * @since 3.24.0 - * @version 3.24.0 - */ - public function test_get_grade() { - - $student = $this->get_mock_student(); - $course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] ); - - $student->enroll( $course->get( 'id' ) ); - - // no grade yet - $this->assertEquals( 'N/A', $student->get_grade( $course->get( 'id' ) ) ); - - $possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ); - $lesson_grades = array(); - - foreach ( $course->get_lessons() as $i => $lesson ) { - - // calculate the ongoing grade as quizzes are completed - if ( 0 !== $i ) { - $this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ), false ) ); - } - - // no grade on the lesson yet - $this->assertEquals( 'N/A', $student->get_grade( $lesson->get( 'id' ) ) ); - - $quiz_id = $lesson->get( 'quiz' ); - if ( ! $quiz_id ) { - continue; - } - - $grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ]; - $this->take_quiz( $quiz_id, $student->get( 'id' ), $grade ); - $this->assertEquals( 'N/A', $student->get_grade( $lesson->get( 'id' ) ) ); // with cache - $this->assertEquals( $grade, $student->get_grade( $lesson->get( 'id' ), false ) ); // no cache - $this->assertEquals( $grade, $student->get_grade( $lesson->get( 'id' ) ) ); // with cache - $lesson_grades[] = $grade; - - } - - // checkout overall course grade once completed - $this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ), false ) ); // no cache - $this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ) ) ); // with cache - - } - - /** - * Test get_progress() - * - * @since 3.15.0 - * @since 5.3.3 Use `assestEqualsWithDelta()`. - * - * @return void - */ - public function test_get_progress() { - - $student = $this->get_mock_student(); - - $courses = $this->generate_mock_courses( 3, 2, 5, 0 ); - - // create a track and add all 3 courses to it - $track_id = wp_insert_term( 'Test Course Track', 'course_track' )['term_id']; - foreach ( $courses as $cid ) { - wp_set_post_terms( $cid, array( $track_id ), 'course_track' ); - } - - // course for most of our tests - $course_id = $courses[0]; - $course = llms_get_post( $course_id ); - - // check progress through course - $i = 0; - while ( $i <= 100 ) { - - $this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), $i ); - $this->assertEquals( $i, $student->get_progress( $course_id, 'course' ) ); - - $i += 10; - - } - - // check track progress - $this->assertEqualsWithDelta( 33.33, $student->get_progress( $track_id, 'course_track' ), 0.01 ); - $this->complete_courses_for_student( $student->get( 'id' ), array( $courses[1], $courses[2] ), 100 ); - $this->assertEqualsWithDelta( 100, $student->get_progress( $track_id, 'course_track' ), 0.01 ); - - // test the progress through a section - $student = $this->get_mock_student(); - foreach ( $course->get_sections( 'ids' ) as $i => $section_id ) { - - $this->assertEquals( 0, $student->get_progress( $section_id, 'section' ) ); - - if ( 0 === $i ) { - $this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), 50 ); - $this->assertEquals( 100, $student->get_progress( $section_id, 'section' ) ); - } else { - $this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), 80 ); - $this->assertEquals( 60, $student->get_progress( $section_id, 'section' ) ); - } - - } - - } - - /** - * Test LLMS_Student::get_registration_date(). - * - * @since 5.2.0 - */ - public function test_get_registration_date() { - - $tests = array( - 'UTC@20' => array( - 'timezone_string' => 'UTC', - 'user_registered' => '2021-01-10 20:00:00', - 'expected_registration_date' => '2021-01-10', - ), - 'UTC@04' => array( - 'timezone_string' => 'UTC', - 'user_registered' => '2021-01-10 04:00:00', - 'expected_registration_date' => '2021-01-10', - ), - '-5@20' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-06-10 20:00:00', - 'expected_registration_date' => '2021-06-10', - ), - '-5@04' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-06-10 04:00:00', - 'expected_registration_date' => '2021-06-09', - ), - '-5@05' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-06-10 05:00:00', - 'expected_registration_date' => '2021-06-10', - ), - '-6@20' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-01-10 20:00:00', - 'expected_registration_date' => '2021-01-10', - ), - '-6@04' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-01-10 04:00:00', - 'expected_registration_date' => '2021-01-09', - ), - '-6@06' => array( - 'timezone_string' => 'America/Chicago', - 'user_registered' => '2021-01-10 06:00:00', - 'expected_registration_date' => '2021-01-10', - ), - '+9@20' => array( - 'timezone_string' => 'Asia/Tokyo', - 'user_registered' => '2021-08-10 20:00:00', - 'expected_registration_date' => '2021-08-11', - ), - '+9@04' => array( - 'timezone_string' => 'Asia/Tokyo', - 'user_registered' => '2021-08-10 04:00:00', - 'expected_registration_date' => '2021-08-10', - ), - ); - - foreach ( $tests as $test_name => $test ) { - # Set the server's local time zone. - update_option( 'timezone_string', $test['timezone_string'] ); - - # Register a new student. - $user_id = $this->factory->user->create( array( - 'role' => 'student', - 'user_registered' => $test['user_registered'], - ) ); - $student = llms_get_student( $user_id ); - - # Test. - $actual_registration_date = $student->get_registration_date( 'Y-m-d' ); - $this->assertEquals( $test['expected_registration_date'], $actual_registration_date, $test_name ); - } - - } - - /** - * Test is_enrolled() method - * - * @since 3.25.0 - * @since 3.33.0 Add test after enrollment deletion. - * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion. - * - * @return void - */ - public function test_is_enrolled() { - - $courses = $this->generate_mock_courses( 3, 1, 1, 0 ); - - $course = llms_get_post( $courses[0] ); - $student = llms_get_student( $this->factory->user->create( array( 'role' => 'student' ) ) ); - - // no status. - $this->assertFalse( $student->is_enrolled( $courses[0] ) ); - $this->assertFalse( $student->is_enrolled( array( $courses[0] ) ) ); - - // enrolled. - $student->enroll( $courses[0] ); - $this->assertTrue( $student->is_enrolled( $courses[0] ) ); - $this->assertTrue( $student->is_enrolled( array( $courses[0] ) ) ); - // check from a lesson. - $this->assertTrue( $student->is_enrolled( $course->get_lessons( 'ids' )[0] ) ); - $this->assertTrue( $student->is_enrolled( array( $course->get_lessons( 'ids' )[0] ) ) ); - - // Enrolled in only one of the specified. - $this->assertFalse( $student->is_enrolled( $courses, 'all' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - - // Enrolled in 2 of the 3. - $student->enroll( $courses[1] ); - $this->assertFalse( $student->is_enrolled( $courses, 'all' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - - // Enrolled in all courses. - $student->enroll( $courses[2] ); - $this->assertTrue( $student->is_enrolled( $courses, 'all' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - - $this->assertTrue( $student->is_enrolled( array( $courses[0], $courses[2] ) ) ); - $this->assertTrue( $student->is_enrolled( $courses[1], 'any' ) ); - - sleep( 1 ); - - // expired. - $student->unenroll( $courses[0] ); - $this->assertFalse( $student->is_enrolled( $courses[0] ) ); - $this->assertFalse( $student->is_enrolled( array( $courses[0] ) ) ); - - $this->assertFalse( $student->is_enrolled( $courses ) ); // default - $this->assertFalse( $student->is_enrolled( $courses, 'all' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - - $student->unenroll( $courses[2] ); - $this->assertFalse( $student->is_enrolled( $courses, 'all' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - $this->assertTrue( $student->is_enrolled( $courses, 'any' ) ); - - $student->unenroll( $courses[1] ); - $this->assertFalse( $student->is_enrolled( $courses, 'any' ) ); - $this->assertFalse( $student->is_enrolled( $courses, 'all' ) ); - - // deleted. - $student->enroll( $courses[1] ); - $student->delete_enrollment( $courses[1] ); - $this->assertFalse( $student->is_enrolled( $courses[1] ) ); - - // Test auto-enrollments deletion. - // create a membership. - $membership = new LLMS_Membership( 'new', 'Membership Title' ); - $membership_id = $membership->get('id'); - - // set the courses as membership auto-enrollments. - $courses = $this->generate_mock_courses( 3, 0, 0, 0 ); - $membership->set( 'auto_enroll', $courses ); - - $student->enroll( $membership_id ); - - $student->delete_enrollment( $membership_id ); - $this->assertFalse( $student->is_enrolled( $membership_id ) ); - $this->assertFalse( $student->is_enrolled( $courses[0] ) ); - $this->assertFalse( $student->is_enrolled( $courses[1] ) ); - $this->assertFalse( $student->is_enrolled( $courses[2] ) ); - - } - -} diff --git a/tests/phpunit/unit-tests/user/class-llms-test-user-permissions.php b/tests/phpunit/unit-tests/user/class-llms-test-user-permissions.php deleted file mode 100644 index d08e79dd30..0000000000 --- a/tests/phpunit/unit-tests/user/class-llms-test-user-permissions.php +++ /dev/null @@ -1,430 +0,0 @@ -<?php -/** - * Test User Permissions and capabilities - * - * @package LifterLMS_Tests/Tests - * - * @group user_permissions - * - * @since 3.34.0 - */ -class LLMS_Test_User_Permissions extends LLMS_UnitTestCase { - - /** - * Setup the test case - * - * @since 3.34.0 - * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes. - * - * @return void - */ - public function set_up() { - parent::set_up(); - $this->obj = new LLMS_User_Permissions(); - } - - /** - * Create mock users of different roles for testing permissions. - * - * @since 3.34.0 - * - * @return int[] - */ - private function create_mock_users() { - - return array( - 'student' => $this->factory->student->create(), - 'admin' => $this->factory->user->create( array( 'role' => 'administrator' ) ), - 'admin2' => $this->factory->user->create( array( 'role' => 'administrator' ) ), - 'editor' => $this->factory->user->create( array( 'role' => 'editor' ) ), - 'subscriber' => $this->factory->user->create( array( 'role' => 'subscriber' ) ), - 'lms_manager' => $this->factory->user->create( array( 'role' => 'lms_manager' ) ), - 'instructor' => $this->factory->user->create( array( 'role' => 'instructor' ) ), - 'assistant' => $this->factory->user->create( array( 'role' => 'instructors_assistant' ) ), - ); - - } - - /** - * Test the get_editable_roles method. - * - * @since 3.34.0 - * - * @return void - */ - public function test_get_editable_roles() { - - $roles = LLMS_User_Permissions::get_editable_roles(); - $this->assertEquals( array( 'instructor', 'instructors_assistant', 'lms_manager', 'student' ), $roles['lms_manager'] ); - $this->assertEquals( array( 'instructors_assistant' ), $roles['instructor'] ); - - } - - /** - * Test the is_current_user_instructor() method. - * - * @since 3.34.0 - * - * @return void - */ - public function test_is_current_user_instructor() { - - $users = $this->create_mock_users(); - - // Obviously not golfers. - foreach ( array( 'admin', 'student', 'editor', 'subscriber', 'lms_manager', 'assistant' ) as $role ) { - wp_set_current_user( $users[ $role ] ); - $this->assertFalse( LLMS_User_Permissions::is_current_user_instructor() ); - } - - // Winner. - wp_set_current_user( $users['instructor'] ); - $this->assertTrue( LLMS_User_Permissions::is_current_user_instructor() ); - - // Logged out. - wp_set_current_user( null ); - $this->assertFalse( LLMS_User_Permissions::is_current_user_instructor() ); - - } - - /** - * Test the user_can_manage_user method. - * - * @since 3.34.0 - * @since 3.41.0 Add tests to ensure admins can still manage other admins. - * - * @return void - */ - public function test_user_can_manage_user() { - - extract( $this->create_mock_users() ); - - // WP Core roles are skipped. - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $student ) ) ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $admin2 ) ) ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $editor ) ) ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $editor, $student ) ) ); - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $subscriber, $student ) ) ); - - // LMS Managers can't manage WP core roles. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $admin ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $editor ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $subscriber ) ) ); - - // LMS Managers can manage all LMS Roles (including other LMS Managers). - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $student ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $instructor ) ) ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $assistant ) ) ); - $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' ) ) ) ) ); - - // Instructor's cannot manage WP core roles. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $admin ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $editor ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $subscriber ) ) ); - - // Instructor's cannot manage LMS Managers or students - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $lms_manager ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $student ) ) ); - - // Instructors can only manage assistants who they "own". - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $assistant ) ) ); - - $ass_obj = llms_get_instructor( $assistant ); - $ass_obj->add_parent( $instructor ); - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $assistant ) ) ); - - // Assistant's cannot manage anything. - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $admin ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $editor ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $subscriber ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $student ) ) ); - $this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $instructor ) ) ); - - // All LMS Roles can manage themselves. - foreach( array( $lms_manager, $instructor, $assistant ) as $uid ) { - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $uid, $uid ) ) ); - } - - } - - /** - * Test the user_can_manage_user() for users with multiple roles. - * - * @since 3.41.0 - * - * @return void - */ - public function test_user_can_manage_user_multiple_roles() { - - extract( $this->create_mock_users() ); - - $admin = new WP_User( $admin ); - $admin->add_role( 'student' ); - - // Admin with student role. - $this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin->ID, $student ) ) ); - - $lms_manager = new WP_User( $lms_manager ); - $lms_manager->add_role( 'student' ); - - $this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager->ID, $student ) ) ); - - } - - /** - * Test the editable_roles() filter for users with single roles. - * - * @since 4.10.0 - * - * @return void - */ - public function test_editable_roles_single_role() { - - $users = $this->create_mock_users(); - - $all_roles = wp_roles()->roles; - - $editable_roles = LLMS_Unit_Test_Util::call_method( $this->obj, 'get_editable_roles'); - - wp_set_current_user( $users['lms_manager'] ); - $lms_manager_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that lms_managers can edit mapped roles. - foreach ( $editable_roles['lms_manager'] as $editable_role ) { - $this->assertContains( $editable_role, $lms_manager_editable_roles ); - } - - wp_set_current_user( $users['instructor'] ); - $instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that instructor can edit mapped roles. - foreach ( $editable_roles['instructor'] as $editable_role ) { - $this->assertContains( $editable_role, $instructor_editable_roles ); - } - - wp_set_current_user( $users['assistant'] ); - $assistant_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that assistants can edit all roles. - foreach ( array_keys( $all_roles ) as $role ) { - $this->assertContains( $role, $assistant_editable_roles ); - } - - wp_set_current_user( $users['admin'] ); - $administrator_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that administrator can edit all roles. - foreach ( array_keys( $all_roles ) as $role ) { - $this->assertContains( $role, $administrator_editable_roles ); - } - } - - /** - * Test the editable_roles() filter for users with multiple roles. - * - * @since 4.10.0 - * - * @return void - */ - public function test_editable_roles_multiple_roles() { - - $users = $this->create_mock_users(); - - $all_roles = wp_roles()->roles; - - wp_set_current_user( $users['lms_manager'] ); - $lms_manager_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - wp_set_current_user( $users['instructor'] ); - $instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - wp_set_current_user( $users['lms_manager'] ); - $user = wp_get_current_user(); - $user->add_role( 'instructor' ); - $lms_manager_instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that lms_manager with instructor role has editable roles from both roles. - foreach ( $lms_manager_editable_roles as $lms_manager_editable_role ) { - $this->assertContains( $lms_manager_editable_role, $lms_manager_instructor_editable_roles ); - } - foreach ( $instructor_editable_roles as $instructor_editable_role ) { - $this->assertContains( $instructor_editable_role, $lms_manager_instructor_editable_roles ); - } - - wp_set_current_user( $users['admin'] ); - $user = wp_get_current_user(); - $user->add_role( 'instructor' ); - $administrator_instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) ); - - // Assert that administrator with instructor role can edit all roles. - foreach ( array_keys( $all_roles ) as $role ) { - $this->assertContains( $role, $administrator_instructor_editable_roles ); - } - } - - /** - * Test student CRUD capabilities - * - * @since Unknown - * - * @return void - */ - public function test_student_crud_caps() { - - $users = $this->create_mock_users(); - - // These users have all student permissions regardless of the user role. - foreach ( array( 'admin', 'lms_manager' ) as $role ) { - - wp_set_current_user( $users[ $role ] ); - $this->assertTrue( current_user_can( 'create_students' ) ); - foreach ( $users as $user ) { - // General Capability. - $this->assertTrue( current_user_can( 'view_students' ) ); - $this->assertTrue( current_user_can( 'edit_students' ) ); - $this->assertTrue( current_user_can( 'delete_students' ) ); - // Specific User. - $this->assertTrue( current_user_can( 'view_students', $user ) ); - $this->assertTrue( current_user_can( 'edit_students', $user ) ); - $this->assertTrue( current_user_can( 'delete_students', $user ) ); - } - - } - - // These users can't do anything. - foreach ( array( 'student', 'editor', 'subscriber' ) as $role ) { - - wp_set_current_user( $users[ $role ] ); - $this->assertFalse( current_user_can( 'create_students' ) ); - - foreach ( $users as $user ) { - // General Capability. - $this->assertFalse( current_user_can( 'view_students' ) ); - $this->assertFalse( current_user_can( 'edit_students' ) ); - $this->assertFalse( current_user_can( 'delete_students' ) ); - // Specific User. - $this->assertFalse( current_user_can( 'view_students', $user ) ); - $this->assertFalse( current_user_can( 'edit_students', $user ) ); - $this->assertFalse( current_user_can( 'delete_students', $user ) ); - } - - } - - $course_1 = $this->factory->course->create_and_get( array( 'sections' => 0 ) ); - $course_2 = $this->factory->course->create_and_get( array( 'sections' => 0 ) ); - - // These users can view their own and that's it. - foreach ( array( 'assistant', 'instructor' ) as $role ) { - - wp_set_current_user( $users[ $role ] ); - $this->assertFalse( current_user_can( 'create_students' ) ); - - foreach ( $users as $user ) { - // General Capability. - $this->assertTrue( current_user_can( 'view_students' ) ); - $this->assertFalse( current_user_can( 'edit_students' ) ); - $this->assertFalse( current_user_can( 'delete_students' ) ); - // Specific User. - $this->assertFalse( current_user_can( 'view_students', $user ) ); - $this->assertFalse( current_user_can( 'edit_students', $user ) ); - $this->assertFalse( current_user_can( 'delete_students', $user ) ); - } - - $course_1->instructors()->set_instructors( array( array( 'id' => $users[ $role ] ) ) ); - $course_2->instructors()->set_instructors( array( array( 'id' => $users[ $role ] ) ) ); - - foreach ( $users as $user ) { - - llms_enroll_student( $user, $course_1->get( 'id' ) ); - $this->assertTrue( current_user_can( 'view_students', $user ) ); - - } - - } - - } - - /** - * Test view_grades capability errors - * - * @since 4.21.2 - * - * @return void - */ - public function test_view_grades_cap_errs() { - - // Logged out user. - $this->assertFalse( current_user_can( 'view_grades' ) ); - - wp_set_current_user( $this->factory->user->create() ); - - // Missing required args. - $this->assertFalse( current_user_can( 'view_grades' ) ); - - } - - /** - * Test view_grades cap in various scenarios and different user types - * - * @since 4.21.2 - * - * @return void - */ - public function test_view_grades_cap() { - - $users = $this->create_mock_users(); - $course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) ); - $quiz = $course->get_lessons()[0]->get_quiz(); - $quiz_id = $quiz->get( 'id' ); - - $users['student2'] = $this->factory->user->create( array( 'role' => 'student' ) ); - $users['student3'] = $this->factory->user->create( array( 'role' => 'student' ) ); - - llms_enroll_student( $users['student'], $course->get( 'id' ) ); - llms_enroll_student( $users['student2'], $course->get( 'id' ) ); - - // Can view anyone's grades. - foreach ( array( 'admin', 'lms_manager' ) as $current_role ) { - wp_set_current_user( $users[ $current_role ] ); - foreach ( $users as $uid ) { - $this->assertTrue( current_user_can( 'view_grades', $uid, $quiz_id ) ); - } - } - - // Can't view other people's grades. - foreach ( array( 'editor', 'subscriber', 'instructor', 'assistant', 'student2' ) as $role ) { - wp_set_current_user( $users[ $role ] ); - - // No for others. - $this->assertFalse( current_user_can( 'view_grades', $users['student'], $quiz_id ), $role ); - - // Yes for their own. - $this->assertTrue( current_user_can( 'view_grades', $users[ $role ], $quiz_id ), $role ); - - } - - // Instructors can view their own students. - $assistant = llms_get_instructor( $users['assistant'] ); - $assistant->add_parent( $users['instructor'] ); - - $course->instructors()->set_instructors( array( - array( 'id' => $users['instructor'] ), - array( 'id' => $users['assistant'] ), - ) ); - - // Can view grades for their students. - foreach ( array( 'instructor', 'assistant' ) as $role ) { - - wp_set_current_user( $users[ $role ] ); - - $this->assertTrue( current_user_can( 'view_grades', $users['student'], $quiz_id ), $role ); - $this->assertTrue( current_user_can( 'view_grades', $users['student2'], $quiz_id ), $role ); - - $this->assertFalse( current_user_can( 'view_grades', $users['student3'], $quiz_id ), $role ); - - } - - } - - -} diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000000..17143a2eea --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInit6fc03334c7d54093e57986a739fada1b::getLoader(); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php new file mode 100644 index 0000000000..afef3fa2ad --- /dev/null +++ b/vendor/composer/ClassLoader.php @@ -0,0 +1,572 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array<string, array<string, int>> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array<string, array<int, string>> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array<string, string> + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array<string, array<string, string[]>> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array<string, string> + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array<string, string> + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array<string, bool> + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array<string, array<int, string>> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array<string, string> + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array<string, string> + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array<string, string> + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array<string, string> $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + * @private + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000000..d50e0c9fcc --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,350 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = require __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + $installed[] = self::$installed; + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000000..f27399a042 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission 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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE 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. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000000..3f4f7db9d2 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,12 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'WP_Async_Request' => $vendorDir . '/deliciousbrains/wp-background-processing/classes/wp-async-request.php', + 'WP_Background_Process' => $vendorDir . '/deliciousbrains/wp-background-processing/classes/wp-background-process.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000000..b7fc0125db --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000000..0412a203b1 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,12 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'LifterLMS\\CLI\\' => array($baseDir . '/libraries/lifterlms-cli/src'), + 'LLMS\\' => array($baseDir . '/includes'), + 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000000..654ff65a52 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,57 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInit6fc03334c7d54093e57986a739fada1b +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + /** + * @return \Composer\Autoload\ClassLoader + */ + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + require __DIR__ . '/platform_check.php'; + + spl_autoload_register(array('ComposerAutoloaderInit6fc03334c7d54093e57986a739fada1b', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); + spl_autoload_unregister(array('ComposerAutoloaderInit6fc03334c7d54093e57986a739fada1b', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit6fc03334c7d54093e57986a739fada1b::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000000..e3c1987c13 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,51 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInit6fc03334c7d54093e57986a739fada1b +{ + public static $prefixLengthsPsr4 = array ( + 'L' => + array ( + 'LifterLMS\\CLI\\' => 14, + 'LLMS\\' => 5, + ), + 'C' => + array ( + 'Composer\\Installers\\' => 20, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'LifterLMS\\CLI\\' => + array ( + 0 => __DIR__ . '/../..' . '/libraries/lifterlms-cli/src', + ), + 'LLMS\\' => + array ( + 0 => __DIR__ . '/../..' . '/includes', + ), + 'Composer\\Installers\\' => + array ( + 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'WP_Async_Request' => __DIR__ . '/..' . '/deliciousbrains/wp-background-processing/classes/wp-async-request.php', + 'WP_Background_Process' => __DIR__ . '/..' . '/deliciousbrains/wp-background-processing/classes/wp-background-process.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit6fc03334c7d54093e57986a739fada1b::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit6fc03334c7d54093e57986a739fada1b::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit6fc03334c7d54093e57986a739fada1b::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000000..02a96b2ce4 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,415 @@ +{ + "packages": [ + { + "name": "composer/installers", + "version": "v1.9.0", + "version_normalized": "1.9.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || 2.0.*@dev", + "composer/semver": "1.0.* || 2.0.*@dev", + "phpunit/phpunit": "^4.8.36", + "sebastian/comparator": "^1.2.4", + "symfony/process": "^2.3" + }, + "time": "2020-04-07T06:57:05+00:00", + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "modulework", + "modx", + "moodle", + "osclass", + "phpbb", + "piwik", + "ppi", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.9.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "install-path": "./installers" + }, + { + "name": "deliciousbrains/wp-background-processing", + "version": "1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/deliciousbrains/wp-background-processing.git", + "reference": "2cbee1abd1b49e1133cd8f611df4d4fc5a8b9800" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deliciousbrains/wp-background-processing/zipball/2cbee1abd1b49e1133cd8f611df4d4fc5a8b9800", + "reference": "2cbee1abd1b49e1133cd8f611df4d4fc5a8b9800", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "suggest": { + "coenjacobs/mozart": "Easily wrap this library with your own prefix, to prevent collisions when multiple plugins use this library" + }, + "time": "2020-07-31T07:00:11+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "classes/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Delicious Brains", + "email": "nom@deliciousbrains.com" + } + ], + "description": "WP Background Processing can be used to fire off non-blocking asynchronous requests or as a background processing tool, allowing you to queue tasks.", + "support": { + "issues": "https://github.com/deliciousbrains/wp-background-processing/issues", + "source": "https://github.com/deliciousbrains/wp-background-processing/tree/master" + }, + "install-path": "../deliciousbrains/wp-background-processing" + }, + { + "name": "lifterlms/lifterlms-blocks", + "version": "2.3.2", + "version_normalized": "2.3.2.0", + "source": { + "type": "git", + "url": "https://github.com/gocodebox/lifterlms-blocks.git", + "reference": "d50aaea00e344e5bd60edb4d7e265bf8c56df77b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gocodebox/lifterlms-blocks/zipball/d50aaea00e344e5bd60edb4d7e265bf8c56df77b", + "reference": "d50aaea00e344e5bd60edb4d7e265bf8c56df77b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "lifterlms/lifterlms-cs": "dev-trunk", + "lifterlms/lifterlms-tests": "^3.1.0" + }, + "time": "2022-02-22T18:34:33+00:00", + "type": "wordpress-plugin", + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Team LifterLMS", + "email": "team@lifterlms.com" + } + ], + "description": "WordPress Editor (Gutenberg) blocks for LifterLMS.", + "support": { + "issues": "https://github.com/gocodebox/lifterlms-blocks/issues", + "source": "https://github.com/gocodebox/lifterlms-blocks/tree/2.3.2" + }, + "install-path": "../../libraries/lifterlms-blocks" + }, + { + "name": "lifterlms/lifterlms-cli", + "version": "0.0.3", + "version_normalized": "0.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/gocodebox/lifterlms-cli.git", + "reference": "543b36dfeeb9a2b2298f236b1200c2128fe14bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gocodebox/lifterlms-cli/zipball/543b36dfeeb9a2b2298f236b1200c2128fe14bbf", + "reference": "543b36dfeeb9a2b2298f236b1200c2128fe14bbf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "lifterlms/lifterlms-cs": "dev-trunk", + "lifterlms/lifterlms-tests": "^3.1.0" + }, + "time": "2021-11-03T17:48:53+00:00", + "bin": [ + "bin/run-behat-tests" + ], + "type": "wordpress-plugin", + "installation-source": "dist", + "autoload": { + "psr-4": { + "LifterLMS\\CLI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Team LifterLMS", + "email": "team@lifterlms.com" + } + ], + "description": "WP CLI commands for LifterLMS", + "support": { + "issues": "https://github.com/gocodebox/lifterlms-cli/issues", + "source": "https://github.com/gocodebox/lifterlms-cli/tree/0.0.3" + }, + "install-path": "../../libraries/lifterlms-cli" + }, + { + "name": "lifterlms/lifterlms-helper", + "version": "3.4.1", + "version_normalized": "3.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/gocodebox/lifterlms-helper.git", + "reference": "f4159a428e2e68d05aabba51f35fe0314c9a4ed8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gocodebox/lifterlms-helper/zipball/f4159a428e2e68d05aabba51f35fe0314c9a4ed8", + "reference": "f4159a428e2e68d05aabba51f35fe0314c9a4ed8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "lifterlms/lifterlms-cs": "dev-trunk", + "lifterlms/lifterlms-tests": "dev-trunk" + }, + "time": "2021-08-17T17:50:29+00:00", + "type": "wordpress-plugin", + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Team LifterLMS", + "email": "team@lifterlms.com" + } + ], + "description": "Update, install, and beta test LifterLMS and LifterLMS add-ons", + "support": { + "issues": "https://github.com/gocodebox/lifterlms-helper/issues", + "source": "https://github.com/gocodebox/lifterlms-helper/tree/3.4.1" + }, + "install-path": "../../libraries/lifterlms-helper" + }, + { + "name": "lifterlms/lifterlms-rest", + "version": "1.0.0-beta.21", + "version_normalized": "1.0.0.0-beta21", + "source": { + "type": "git", + "url": "https://github.com/gocodebox/lifterlms-rest.git", + "reference": "8c13967bde439c5c6b3438bd44ce007bd9334e00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gocodebox/lifterlms-rest/zipball/8c13967bde439c5c6b3438bd44ce007bd9334e00", + "reference": "8c13967bde439c5c6b3438bd44ce007bd9334e00", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "lifterlms/lifterlms-cs": "dev-trunk", + "lifterlms/lifterlms-tests": "^3.1.0" + }, + "time": "2021-12-07T18:32:53+00:00", + "type": "wordpress-plugin", + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Team LifterLMS", + "email": "team@lifterlms.com" + } + ], + "description": "REST API feature plugin for the LifterLMS Core.", + "support": { + "issues": "https://github.com/gocodebox/lifterlms-rest/issues", + "source": "https://github.com/gocodebox/lifterlms-rest/tree/1.0.0-beta.21" + }, + "install-path": "../../libraries/lifterlms-rest" + }, + { + "name": "woocommerce/action-scheduler", + "version": "3.4.0", + "version_normalized": "3.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/action-scheduler.git", + "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/3218a33ff14b968f8cb05de9656c2efa1eeb1330", + "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^7.5", + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-cli/wp-cli": "~2.5.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "time": "2021-10-28T17:09:12+00:00", + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "Action Scheduler for WordPress and WooCommerce", + "homepage": "https://actionscheduler.org/", + "support": { + "issues": "https://github.com/woocommerce/action-scheduler/issues", + "source": "https://github.com/woocommerce/action-scheduler/tree/3.4.0" + }, + "install-path": "../woocommerce/action-scheduler" + } + ], + "dev": false, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000000..c1a087cd74 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,98 @@ +<?php return array( + 'root' => array( + 'pretty_version' => 'dev-trunk', + 'version' => 'dev-trunk', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '401493a05e233d6ea0ca982d5d8e3ebff41b8880', + 'name' => 'gocodebox/lifterlms', + 'dev' => false, + ), + 'versions' => array( + 'composer/installers' => array( + 'pretty_version' => 'v1.9.0', + 'version' => '1.9.0.0', + 'type' => 'composer-plugin', + 'install_path' => __DIR__ . '/./installers', + 'aliases' => array(), + 'reference' => 'b93bcf0fa1fccb0b7d176b0967d969691cd74cca', + 'dev_requirement' => false, + ), + 'deliciousbrains/wp-background-processing' => array( + 'pretty_version' => '1.0.2', + 'version' => '1.0.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../deliciousbrains/wp-background-processing', + 'aliases' => array(), + 'reference' => '2cbee1abd1b49e1133cd8f611df4d4fc5a8b9800', + 'dev_requirement' => false, + ), + 'gocodebox/lifterlms' => array( + 'pretty_version' => 'dev-trunk', + 'version' => 'dev-trunk', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'reference' => '401493a05e233d6ea0ca982d5d8e3ebff41b8880', + 'dev_requirement' => false, + ), + 'lifterlms/lifterlms-blocks' => array( + 'pretty_version' => '2.3.2', + 'version' => '2.3.2.0', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../libraries/lifterlms-blocks', + 'aliases' => array(), + 'reference' => 'd50aaea00e344e5bd60edb4d7e265bf8c56df77b', + 'dev_requirement' => false, + ), + 'lifterlms/lifterlms-cli' => array( + 'pretty_version' => '0.0.3', + 'version' => '0.0.3.0', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../libraries/lifterlms-cli', + 'aliases' => array(), + 'reference' => '543b36dfeeb9a2b2298f236b1200c2128fe14bbf', + 'dev_requirement' => false, + ), + 'lifterlms/lifterlms-helper' => array( + 'pretty_version' => '3.4.1', + 'version' => '3.4.1.0', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../libraries/lifterlms-helper', + 'aliases' => array(), + 'reference' => 'f4159a428e2e68d05aabba51f35fe0314c9a4ed8', + 'dev_requirement' => false, + ), + 'lifterlms/lifterlms-rest' => array( + 'pretty_version' => '1.0.0-beta.21', + 'version' => '1.0.0.0-beta21', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../../libraries/lifterlms-rest', + 'aliases' => array(), + 'reference' => '8c13967bde439c5c6b3438bd44ce007bd9334e00', + 'dev_requirement' => false, + ), + 'roundcube/plugin-installer' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'shama/baton' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'woocommerce/action-scheduler' => array( + 'pretty_version' => '3.4.0', + 'version' => '3.4.0.0', + 'type' => 'wordpress-plugin', + 'install_path' => __DIR__ . '/../woocommerce/action-scheduler', + 'aliases' => array(), + 'reference' => '3218a33ff14b968f8cb05de9656c2efa1eeb1330', + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000000..92370c5a0c --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ +<?php + +// platform_check.php @generated by Composer + +$issues = array(); + +if (!(PHP_VERSION_ID >= 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/deliciousbrains/wp-background-processing/classes/wp-async-request.php b/vendor/deliciousbrains/wp-background-processing/classes/wp-async-request.php new file mode 100644 index 0000000000..7a37dd687c --- /dev/null +++ b/vendor/deliciousbrains/wp-background-processing/classes/wp-async-request.php @@ -0,0 +1,181 @@ +<?php +/** + * WP Async Request + * + * @package WP-Background-Processing + */ + +/** + * Abstract WP_Async_Request class. + * + * @abstract + */ +abstract class WP_Async_Request { + + /** + * Prefix + * + * (default value: 'wp') + * + * @var string + * @access protected + */ + protected $prefix = 'wp'; + + /** + * Action + * + * (default value: 'async_request') + * + * @var string + * @access protected + */ + protected $action = 'async_request'; + + /** + * Identifier + * + * @var mixed + * @access protected + */ + protected $identifier; + + /** + * Data + * + * (default value: array()) + * + * @var array + * @access protected + */ + protected $data = array(); + + /** + * Initiate new async request + */ + public function __construct() { + $this->identifier = $this->prefix . '_' . $this->action; + + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); + } + + /** + * Set data used during the request + * + * @param array $data Data. + * + * @return $this + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } + + /** + * Dispatch the async request + * + * @return array|WP_Error + */ + public function dispatch() { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * Get query args + * + * @return array + */ + protected function get_query_args() { + if ( property_exists( $this, 'query_args' ) ) { + return $this->query_args; + } + + $args = array( + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $url + */ + return apply_filters( $this->identifier . '_query_args', $args ); + } + + /** + * Get query URL + * + * @return string + */ + protected function get_query_url() { + if ( property_exists( $this, 'query_url' ) ) { + return $this->query_url; + } + + $url = admin_url( 'admin-ajax.php' ); + + /** + * Filters the post arguments used during an async request. + * + * @param string $url + */ + return apply_filters( $this->identifier . '_query_url', $url ); + } + + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; + } + + $args = array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $args + */ + return apply_filters( $this->identifier . '_post_args', $args ); + } + + /** + * Maybe handle + * + * Check for correct nonce and pass to handler. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Handle + * + * Override this method to perform any actions required + * during the async request. + */ + abstract protected function handle(); + +} diff --git a/vendor/deliciousbrains/wp-background-processing/classes/wp-background-process.php b/vendor/deliciousbrains/wp-background-processing/classes/wp-background-process.php new file mode 100644 index 0000000000..ce7904a16c --- /dev/null +++ b/vendor/deliciousbrains/wp-background-processing/classes/wp-background-process.php @@ -0,0 +1,505 @@ +<?php +/** + * WP Background Process + * + * @package WP-Background-Processing + */ + +/** + * Abstract WP_Background_Process class. + * + * @abstract + * @extends WP_Async_Request + */ +abstract class WP_Background_Process extends WP_Async_Request { + + /** + * Action + * + * (default value: 'background_process') + * + * @var string + * @access protected + */ + protected $action = 'background_process'; + + /** + * Start time of current process. + * + * (default value: 0) + * + * @var int + * @access protected + */ + protected $start_time = 0; + + /** + * Cron_hook_identifier + * + * @var mixed + * @access protected + */ + protected $cron_hook_identifier; + + /** + * Cron_interval_identifier + * + * @var mixed + * @access protected + */ + protected $cron_interval_identifier; + + /** + * Initiate new background process + */ + public function __construct() { + parent::__construct(); + + $this->cron_hook_identifier = $this->identifier . '_cron'; + $this->cron_interval_identifier = $this->identifier . '_cron_interval'; + + add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); + add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); + } + + /** + * Dispatch + * + * @access public + * @return void + */ + public function dispatch() { + // Schedule the cron healthcheck. + $this->schedule_event(); + + // Perform remote post. + return parent::dispatch(); + } + + /** + * Push to queue + * + * @param mixed $data Data. + * + * @return $this + */ + public function push_to_queue( $data ) { + $this->data[] = $data; + + return $this; + } + + /** + * Save queue + * + * @return $this + */ + public function save() { + $key = $this->generate_key(); + + if ( ! empty( $this->data ) ) { + update_site_option( $key, $this->data ); + } + + return $this; + } + + /** + * Update queue + * + * @param string $key Key. + * @param array $data Data. + * + * @return $this + */ + public function update( $key, $data ) { + if ( ! empty( $data ) ) { + update_site_option( $key, $data ); + } + + return $this; + } + + /** + * Delete queue + * + * @param string $key Key. + * + * @return $this + */ + public function delete( $key ) { + delete_site_option( $key ); + + return $this; + } + + /** + * Generate key + * + * Generates a unique key based on microtime. Queue items are + * given a unique key so that they can be merged upon save. + * + * @param int $length Length. + * + * @return string + */ + protected function generate_key( $length = 64 ) { + $unique = md5( microtime() . rand() ); + $prepend = $this->identifier . '_batch_'; + + return substr( $prepend . $unique, 0, $length ); + } + + /** + * Maybe process queue + * + * Checks whether data exists within the queue and that + * the process is not already running. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + if ( $this->is_process_running() ) { + // Background process already running. + wp_die(); + } + + if ( $this->is_queue_empty() ) { + // No data to process. + wp_die(); + } + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Is queue empty + * + * @return bool + */ + protected function is_queue_empty() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $count = $wpdb->get_var( $wpdb->prepare( " + SELECT COUNT(*) + FROM {$table} + WHERE {$column} LIKE %s + ", $key ) ); + + return ( $count > 0 ) ? false : true; + } + + /** + * Is process running + * + * Check whether the current process is already running + * in a background process. + */ + protected function is_process_running() { + if ( get_site_transient( $this->identifier . '_process_lock' ) ) { + // Process already running. + return true; + } + + return false; + } + + /** + * Lock process + * + * Lock the process so that multiple instances can't run simultaneously. + * Override if applicable, but the duration should be greater than that + * defined in the time_exceeded() method. + */ + protected function lock_process() { + $this->start_time = time(); // Set start time of current process. + + $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute + $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); + + set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); + } + + /** + * Unlock process + * + * Unlock the process so that other instances can spawn. + * + * @return $this + */ + protected function unlock_process() { + delete_site_transient( $this->identifier . '_process_lock' ); + + return $this; + } + + /** + * Get batch + * + * @return stdClass Return the first batch from the queue + */ + protected function get_batch() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + $key_column = 'option_id'; + $value_column = 'option_value'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + $key_column = 'meta_id'; + $value_column = 'meta_value'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $query = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$table} + WHERE {$column} LIKE %s + ORDER BY {$key_column} ASC + LIMIT 1 + ", $key ) ); + + $batch = new stdClass(); + $batch->key = $query->$column; + $batch->data = maybe_unserialize( $query->$value_column ); + + return $batch; + } + + /** + * Handle + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + */ + protected function handle() { + $this->lock_process(); + + do { + $batch = $this->get_batch(); + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; + } else { + unset( $batch->data[ $key ] ); + } + + if ( $this->time_exceeded() || $this->memory_exceeded() ) { + // Batch limits reached. + break; + } + } + + // Update or delete current batch. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } else { + $this->delete( $batch->key ); + } + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + + $this->unlock_process(); + + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); + } + + wp_die(); + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% + * of the maximum WordPress memory. + * + * @return bool + */ + protected function memory_exceeded() { + $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory + $current_memory = memory_get_usage( true ); + $return = false; + + if ( $current_memory >= $memory_limit ) { + $return = true; + } + + return apply_filters( $this->identifier . '_memory_exceeded', $return ); + } + + /** + * Get memory limit + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + // Sensible default. + $memory_limit = '128M'; + } + + if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) { + // Unlimited, set to 32GB. + $memory_limit = '32000M'; + } + + return wp_convert_hr_to_bytes( $memory_limit ); + } + + /** + * Time exceeded. + * + * Ensures the batch never exceeds a sensible time limit. + * A timeout limit of 30s is common on shared hosting. + * + * @return bool + */ + protected function time_exceeded() { + $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds + $return = false; + + if ( time() >= $finish ) { + $return = true; + } + + return apply_filters( $this->identifier . '_time_exceeded', $return ); + } + + /** + * Complete. + * + * Override if applicable, but ensure that the below actions are + * performed, or, call parent::complete(). + */ + protected function complete() { + // Unschedule the cron healthcheck. + $this->clear_scheduled_event(); + } + + /** + * Schedule cron healthcheck + * + * @access public + * + * @param mixed $schedules Schedules. + * + * @return mixed + */ + public function schedule_cron_healthcheck( $schedules ) { + $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + + if ( property_exists( $this, 'cron_interval' ) ) { + $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + } + + // Adds every 5 minutes to the existing schedules. + $schedules[ $this->identifier . '_cron_interval' ] = array( + 'interval' => MINUTE_IN_SECONDS * $interval, + 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), + ); + + return $schedules; + } + + /** + * Handle cron healthcheck + * + * Restart the background process if not already running + * and data exists in the queue. + */ + public function handle_cron_healthcheck() { + if ( $this->is_process_running() ) { + // Background process already running. + exit; + } + + if ( $this->is_queue_empty() ) { + // No data to process. + $this->clear_scheduled_event(); + exit; + } + + $this->handle(); + + exit; + } + + /** + * Schedule event + */ + protected function schedule_event() { + if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { + wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); + } + } + + /** + * Clear scheduled event + */ + protected function clear_scheduled_event() { + $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); + + if ( $timestamp ) { + wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); + } + } + + /** + * Cancel Process + * + * Stop processing queue items, clear cronjob and delete batch. + * + */ + public function cancel_process() { + if ( ! $this->is_queue_empty() ) { + $batch = $this->get_batch(); + + $this->delete( $batch->key ); + + wp_clear_scheduled_hook( $this->cron_hook_identifier ); + } + + } + + /** + * Task + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @param mixed $item Queue item to iterate over. + * + * @return mixed + */ + abstract protected function task( $item ); + +} \ No newline at end of file diff --git a/vendor/deliciousbrains/wp-background-processing/license.txt b/vendor/deliciousbrains/wp-background-processing/license.txt new file mode 100644 index 0000000000..a0939e9214 --- /dev/null +++ b/vendor/deliciousbrains/wp-background-processing/license.txt @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110, USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/vendor/deliciousbrains/wp-background-processing/wp-background-processing.php b/vendor/deliciousbrains/wp-background-processing/wp-background-processing.php new file mode 100644 index 0000000000..c2fc252c09 --- /dev/null +++ b/vendor/deliciousbrains/wp-background-processing/wp-background-processing.php @@ -0,0 +1,24 @@ +<?php +/** + * WP-Background Processing + * + * @package WP-Background-Processing + */ + +/* +Plugin Name: WP Background Processing +Plugin URI: https://github.com/A5hleyRich/wp-background-processing +Description: Asynchronous requests and background processing in WordPress. +Author: Delicious Brains Inc. +Version: 1.0 +Author URI: https://deliciousbrains.com/ +GitHub Plugin URI: https://github.com/A5hleyRich/wp-background-processing +GitHub Branch: master +*/ + +if ( ! class_exists( 'WP_Async_Request' ) ) { + require_once plugin_dir_path( __FILE__ ) . 'classes/wp-async-request.php'; +} +if ( ! class_exists( 'WP_Background_Process' ) ) { + require_once plugin_dir_path( __FILE__ ) . 'classes/wp-background-process.php'; +} diff --git a/vendor/woocommerce/action-scheduler/action-scheduler.php b/vendor/woocommerce/action-scheduler/action-scheduler.php new file mode 100644 index 0000000000..859e4c9ae8 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/action-scheduler.php @@ -0,0 +1,65 @@ +<?php +/** + * Plugin Name: Action Scheduler + * Plugin URI: https://actionscheduler.org + * Description: A robust scheduling library for use in WordPress plugins. + * Author: Automattic + * Author URI: https://automattic.com/ + * Version: 3.4.0 + * License: GPLv3 + * + * Copyright 2019 Automattic, Inc. (https://automattic.com/contact/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * @package ActionScheduler + */ + +if ( ! function_exists( 'action_scheduler_register_3_dot_4_dot_0' ) && function_exists( 'add_action' ) ) { + + if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { + require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; + add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); + } + + add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_4_dot_0', 0, 0 ); + + /** + * Registers this version of Action Scheduler. + */ + function action_scheduler_register_3_dot_4_dot_0() { + $versions = ActionScheduler_Versions::instance(); + $versions->register( '3.4.0', 'action_scheduler_initialize_3_dot_4_dot_0' ); + } + + /** + * Initializes this version of Action Scheduler. + */ + function action_scheduler_initialize_3_dot_4_dot_0() { + // A final safety check is required even here, because historic versions of Action Scheduler + // followed a different pattern (in some unusual cases, we could reach this point and the + // ActionScheduler class is already defined—so we need to guard against that). + if ( ! class_exists( 'ActionScheduler', false ) ) { + require_once __DIR__ . '/classes/abstracts/ActionScheduler.php'; + ActionScheduler::init( __FILE__ ); + } + } + + // Support usage in themes - load this version if no plugin has loaded a version yet. + if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { + action_scheduler_initialize_3_dot_4_dot_0(); + do_action( 'action_scheduler_pre_theme_init' ); + ActionScheduler_Versions::initialize_latest_version(); + } +} diff --git a/vendor/woocommerce/action-scheduler/changelog.txt b/vendor/woocommerce/action-scheduler/changelog.txt new file mode 100644 index 0000000000..4bb2650b78 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/changelog.txt @@ -0,0 +1,45 @@ +*** Changelog *** + += 3.4.0 - 2021-10-29 = +* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 +* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 +* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776 +* Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 +* Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 +* Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 +* Dev - Improve actions table indicies (props @glagonikas). #774 & #777 +* Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 +* Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 +* Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 +* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780 + += 3.3.0 - 2021-09-15 = +* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645 +* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519 +* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645 +* Dev - Now supports queries that use multiple statuses. #649 +* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723 + += 3.2.1 - 2021-06-21 = +* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714 +* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600. + += 3.2.0 - 2021-06-03 = +* Fix - Add "no ordering" option to as_next_scheduled_action(). +* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634. +* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634. +* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas). +* Fix - Fix unit tests infrastructure and adapt tests to PHP 8. +* Fix - Identify in-use data store. +* Fix - Improve test_migration_is_scheduled. +* Fix - PHP notice on list table. +* Fix - Speed up clean up and batch selects. +* Fix - Update pending dependencies. +* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array(). +* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility. +* Fix - add is_initialized() to docs. +* Fix - fix file permissions. +* Fix - fixes #664 by replacing __ with esc_html__. + += 3.1.6 - 2020-05-12 = +* Change log starts. diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionClaim.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionClaim.php new file mode 100644 index 0000000000..8b5681620e --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionClaim.php @@ -0,0 +1,23 @@ +<?php + +/** + * Class ActionScheduler_ActionClaim + */ +class ActionScheduler_ActionClaim { + private $id = ''; + private $action_ids = array(); + + public function __construct( $id, array $action_ids ) { + $this->id = $id; + $this->action_ids = $action_ids; + } + + public function get_id() { + return $this->id; + } + + public function get_actions() { + return $this->action_ids; + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php new file mode 100644 index 0000000000..545277f8ff --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php @@ -0,0 +1,179 @@ +<?php + +/** + * Class ActionScheduler_ActionFactory + */ +class ActionScheduler_ActionFactory { + + /** + * @param string $status The action's status in the data store + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass to callbacks when the hook is triggered + * @param ActionScheduler_Schedule $schedule The action's schedule + * @param string $group A group to put the action in + * + * @return ActionScheduler_Action An instance of the stored action + */ + public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { + + switch ( $status ) { + case ActionScheduler_Store::STATUS_PENDING : + $action_class = 'ActionScheduler_Action'; + break; + case ActionScheduler_Store::STATUS_CANCELED : + $action_class = 'ActionScheduler_CanceledAction'; + if ( ! is_null( $schedule ) && ! is_a( $schedule, 'ActionScheduler_CanceledSchedule' ) && ! is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) { + $schedule = new ActionScheduler_CanceledSchedule( $schedule->get_date() ); + } + break; + default : + $action_class = 'ActionScheduler_FinishedAction'; + break; + } + + $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); + + $action = new $action_class( $hook, $args, $schedule, $group ); + + /** + * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. + * + * @param ActionScheduler_Action $action The instantiated action. + * @param string $hook The instantiated action's hook. + * @param array $args The instantiated action's args. + * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. + * @param string $group The instantiated action's group. + */ + return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group ); + } + + /** + * Enqueue an action to run one time, as soon as possible (rather a specific scheduled time). + * + * This method creates a new action with the NULLSchedule. This schedule maps to a MySQL datetime string of + * 0000-00-00 00:00:00. This is done to create a psuedo "async action" type that is fully backward compatible. + * Existing queries to claim actions claim by date, meaning actions scheduled for 0000-00-00 00:00:00 will + * always be claimed prior to actions scheduled for a specific date. This makes sure that any async action is + * given priority in queue processing. This has the added advantage of making sure async actions can be + * claimed by both the existing WP Cron and WP CLI runners, as well as a new async request runner. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function async( $hook, $args = array(), $group = '' ) { + $schedule = new ActionScheduler_NullSchedule(); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $when Unix timestamp when the action will run + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function single( $hook, $args = array(), $when = null, $group = '' ) { + $date = as_get_datetime_object( $when ); + $schedule = new ActionScheduler_SimpleSchedule( $date ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create the first instance of an action recurring on a given interval. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $first Unix timestamp for the first run + * @param int $interval Seconds between runs + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) { + if ( empty($interval) ) { + return $this->single( $hook, $args, $first, $group ); + } + $date = as_get_datetime_object( $first ); + $schedule = new ActionScheduler_IntervalSchedule( $date, $interval ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create the first instance of an action recurring on a Cron schedule. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $base_timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param int $schedule A cron definition string + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) { + if ( empty($schedule) ) { + return $this->single( $hook, $args, $base_timestamp, $group ); + } + $date = as_get_datetime_object( $base_timestamp ); + $cron = CronExpression::factory( $schedule ); + $schedule = new ActionScheduler_CronSchedule( $date, $cron ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create a successive instance of a recurring or cron action. + * + * Importantly, the action will be rescheduled to run based on the current date/time. + * That means when the action is scheduled to run in the past, the next scheduled date + * will be pushed forward. For example, if a recurring action set to run every hour + * was scheduled to run 5 seconds ago, it will be next scheduled for 1 hour in the + * future, which is 1 hour and 5 seconds from when it was last scheduled to run. + * + * Alternatively, if the action is scheduled to run in the future, and is run early, + * likely via manual intervention, then its schedule will change based on the time now. + * For example, if a recurring action set to run every day, and is run 12 hours early, + * it will run again in 24 hours, not 36 hours. + * + * This slippage is less of an issue with Cron actions, as the specific run time can + * be set for them to run, e.g. 1am each day. In those cases, and entire period would + * need to be missed before there was any change is scheduled, e.g. in the case of an + * action scheduled for 1am each day, the action would need to run an entire day late. + * + * @param ActionScheduler_Action $action The existing action. + * + * @return string The ID of the stored action + * @throws InvalidArgumentException If $action is not a recurring action. + */ + public function repeat( $action ) { + $schedule = $action->get_schedule(); + $next = $schedule->get_next( as_get_datetime_object() ); + + if ( is_null( $next ) || ! $schedule->is_recurring() ) { + throw new InvalidArgumentException( __( 'Invalid action - must be a recurring action.', 'action-scheduler' ) ); + } + + $schedule_class = get_class( $schedule ); + $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); + $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + return $this->store( $new_action ); + } + + /** + * @param ActionScheduler_Action $action + * + * @return int The ID of the stored action + */ + protected function store( ActionScheduler_Action $action ) { + $store = ActionScheduler_Store::instance(); + return $store->save_action( $action ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php new file mode 100644 index 0000000000..c1fd0d72d0 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php @@ -0,0 +1,154 @@ +<?php + +/** + * Class ActionScheduler_AdminView + * @codeCoverageIgnore + */ +class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated { + + private static $admin_view = NULL; + + private static $screen_id = 'tools_page_action-scheduler'; + + /** @var ActionScheduler_ListTable */ + protected $list_table; + + /** + * @return ActionScheduler_AdminView + * @codeCoverageIgnore + */ + public static function instance() { + + if ( empty( self::$admin_view ) ) { + $class = apply_filters('action_scheduler_admin_view_class', 'ActionScheduler_AdminView'); + self::$admin_view = new $class(); + } + + return self::$admin_view; + } + + /** + * @codeCoverageIgnore + */ + public function init() { + if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) { + + if ( class_exists( 'WooCommerce' ) ) { + add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) ); + add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) ); + add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) ); + } + + add_action( 'admin_menu', array( $this, 'register_menu' ) ); + + add_action( 'current_screen', array( $this, 'add_help_tabs' ) ); + } + } + + public function system_status_report() { + $table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() ); + $table->render(); + } + + /** + * Registers action-scheduler into WooCommerce > System status. + * + * @param array $tabs An associative array of tab key => label. + * @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs + */ + public function register_system_status_tab( array $tabs ) { + $tabs['action-scheduler'] = __( 'Scheduled Actions', 'action-scheduler' ); + + return $tabs; + } + + /** + * Include Action Scheduler's administration under the Tools menu. + * + * A menu under the Tools menu is important for backward compatibility (as that's + * where it started), and also provides more convenient access than the WooCommerce + * System Status page, and for sites where WooCommerce isn't active. + */ + public function register_menu() { + $hook_suffix = add_submenu_page( + 'tools.php', + __( 'Scheduled Actions', 'action-scheduler' ), + __( 'Scheduled Actions', 'action-scheduler' ), + 'manage_options', + 'action-scheduler', + array( $this, 'render_admin_ui' ) + ); + add_action( 'load-' . $hook_suffix , array( $this, 'process_admin_ui' ) ); + } + + /** + * Triggers processing of any pending actions. + */ + public function process_admin_ui() { + $this->get_list_table(); + } + + /** + * Renders the Admin UI + */ + public function render_admin_ui() { + $table = $this->get_list_table(); + $table->display_page(); + } + + /** + * Get the admin UI object and process any requested actions. + * + * @return ActionScheduler_ListTable + */ + protected function get_list_table() { + if ( null === $this->list_table ) { + $this->list_table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() ); + $this->list_table->process_actions(); + } + + return $this->list_table; + } + + /** + * Provide more information about the screen and its data in the help tab. + */ + public function add_help_tabs() { + $screen = get_current_screen(); + + if ( ! $screen || self::$screen_id != $screen->id ) { + return; + } + + $as_version = ActionScheduler_Versions::instance()->latest_version(); + $screen->add_help_tab( + array( + 'id' => 'action_scheduler_about', + 'title' => __( 'About', 'action-scheduler' ), + 'content' => + '<h2>' . sprintf( __( 'About Action Scheduler %s', 'action-scheduler' ), $as_version ) . '</h2>' . + '<p>' . + __( 'Action Scheduler is a scalable, traceable job queue for background processing large sets of actions. Action Scheduler works by triggering an action hook to run at some time in the future. Scheduled actions can also be scheduled to run on a recurring schedule.', 'action-scheduler' ) . + '</p>', + ) + ); + + $screen->add_help_tab( + array( + 'id' => 'action_scheduler_columns', + 'title' => __( 'Columns', 'action-scheduler' ), + 'content' => + '<h2>' . __( 'Scheduled Action Columns', 'action-scheduler' ) . '</h2>' . + '<ul>' . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Hook', 'action-scheduler' ), __( 'Name of the action hook that will be triggered.', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Status', 'action-scheduler' ), __( 'Action statuses are Pending, Complete, Canceled, Failed', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Arguments', 'action-scheduler' ), __( 'Optional data array passed to the action hook.', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Group', 'action-scheduler' ), __( 'Optional action group.', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Recurrence', 'action-scheduler' ), __( 'The action\'s schedule frequency.', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Scheduled', 'action-scheduler' ), __( 'The date/time the action is/was scheduled to run.', 'action-scheduler' ) ) . + sprintf( '<li><strong>%1$s</strong>: %2$s</li>', __( 'Log', 'action-scheduler' ), __( 'Activity log for the action.', 'action-scheduler' ) ) . + '</ul>', + ) + ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php new file mode 100644 index 0000000000..57706a24c4 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php @@ -0,0 +1,97 @@ +<?php +/** + * ActionScheduler_AsyncRequest_QueueRunner + */ + +defined( 'ABSPATH' ) || exit; + +/** + * ActionScheduler_AsyncRequest_QueueRunner class. + */ +class ActionScheduler_AsyncRequest_QueueRunner extends WP_Async_Request { + + /** + * Data store for querying actions + * + * @var ActionScheduler_Store + * @access protected + */ + protected $store; + + /** + * Prefix for ajax hooks + * + * @var string + * @access protected + */ + protected $prefix = 'as'; + + /** + * Action for ajax hooks + * + * @var string + * @access protected + */ + protected $action = 'async_request_queue_runner'; + + /** + * Initiate new async request + */ + public function __construct( ActionScheduler_Store $store ) { + parent::__construct(); + $this->store = $store; + } + + /** + * Handle async requests + * + * Run a queue, and maybe dispatch another async request to run another queue + * if there are still pending actions after completing a queue in this request. + */ + protected function handle() { + do_action( 'action_scheduler_run_queue', 'Async Request' ); // run a queue in the same way as WP Cron, but declare the Async Request context + + $sleep_seconds = $this->get_sleep_seconds(); + + if ( $sleep_seconds ) { + sleep( $sleep_seconds ); + } + + $this->maybe_dispatch(); + } + + /** + * If the async request runner is needed and allowed to run, dispatch a request. + */ + public function maybe_dispatch() { + if ( ! $this->allow() ) { + return; + } + + $this->dispatch(); + ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request(); + } + + /** + * Only allow async requests when needed. + * + * Also allow 3rd party code to disable running actions via async requests. + */ + protected function allow() { + + if ( ! has_action( 'action_scheduler_run_queue' ) || ActionScheduler::runner()->has_maximum_concurrent_batches() || ! $this->store->has_pending_actions_due() ) { + $allow = false; + } else { + $allow = true; + } + + return apply_filters( 'action_scheduler_allow_async_request_runner', $allow ); + } + + /** + * Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that. + */ + protected function get_sleep_seconds() { + return apply_filters( 'action_scheduler_async_request_sleep_seconds', 5, $this ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php new file mode 100644 index 0000000000..85e0ed9da3 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php @@ -0,0 +1,109 @@ +<?php + +/** + * Class ActionScheduler_Compatibility + */ +class ActionScheduler_Compatibility { + + /** + * Converts a shorthand byte value to an integer byte value. + * + * Wrapper for wp_convert_hr_to_bytes(), moved to load.php in WordPress 4.6 from media.php + * + * @link https://secure.php.net/manual/en/function.ini-get.php + * @link https://secure.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + * + * @param string $value A (PHP ini) byte value, either shorthand or ordinary. + * @return int An integer byte value. + */ + public static function convert_hr_to_bytes( $value ) { + if ( function_exists( 'wp_convert_hr_to_bytes' ) ) { + return wp_convert_hr_to_bytes( $value ); + } + + $value = strtolower( trim( $value ) ); + $bytes = (int) $value; + + if ( false !== strpos( $value, 'g' ) ) { + $bytes *= GB_IN_BYTES; + } elseif ( false !== strpos( $value, 'm' ) ) { + $bytes *= MB_IN_BYTES; + } elseif ( false !== strpos( $value, 'k' ) ) { + $bytes *= KB_IN_BYTES; + } + + // Deal with large (float) values which run into the maximum integer size. + return min( $bytes, PHP_INT_MAX ); + } + + /** + * Attempts to raise the PHP memory limit for memory intensive processes. + * + * Only allows raising the existing limit and prevents lowering it. + * + * Wrapper for wp_raise_memory_limit(), added in WordPress v4.6.0 + * + * @return bool|int|string The limit that was set or false on failure. + */ + public static function raise_memory_limit() { + if ( function_exists( 'wp_raise_memory_limit' ) ) { + return wp_raise_memory_limit( 'admin' ); + } + + $current_limit = @ini_get( 'memory_limit' ); + $current_limit_int = self::convert_hr_to_bytes( $current_limit ); + + if ( -1 === $current_limit_int ) { + return false; + } + + $wp_max_limit = WP_MAX_MEMORY_LIMIT; + $wp_max_limit_int = self::convert_hr_to_bytes( $wp_max_limit ); + $filtered_limit = apply_filters( 'admin_memory_limit', $wp_max_limit ); + $filtered_limit_int = self::convert_hr_to_bytes( $filtered_limit ); + + if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { + if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) { + return $filtered_limit; + } else { + return false; + } + } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { + if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) { + return $wp_max_limit; + } else { + return false; + } + } + return false; + } + + /** + * Attempts to raise the PHP timeout for time intensive processes. + * + * Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available. + * + * @param int $limit The time limit in seconds. + */ + public static function raise_time_limit( $limit = 0 ) { + $limit = (int) $limit; + $max_execution_time = (int) ini_get( 'max_execution_time' ); + + /* + * If the max execution time is already unlimited (zero), or if it exceeds or is equal to the proposed + * limit, there is no reason for us to make further changes (we never want to lower it). + */ + if ( + 0 === $max_execution_time + || ( $max_execution_time >= $limit && $limit !== 0 ) + ) { + return; + } + + if ( function_exists( 'wc_set_time_limit' ) ) { + wc_set_time_limit( $limit ); + } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved + @set_time_limit( $limit ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DataController.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DataController.php new file mode 100644 index 0000000000..eb69847b5f --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DataController.php @@ -0,0 +1,187 @@ +<?php + +use Action_Scheduler\Migration\Controller; + +/** + * Class ActionScheduler_DataController + * + * The main plugin/initialization class for the data stores. + * + * Responsible for hooking everything up with WordPress. + * + * @package Action_Scheduler + * + * @since 3.0.0 + */ +class ActionScheduler_DataController { + /** Action data store class name. */ + const DATASTORE_CLASS = 'ActionScheduler_DBStore'; + + /** Logger data store class name. */ + const LOGGER_CLASS = 'ActionScheduler_DBLogger'; + + /** Migration status option name. */ + const STATUS_FLAG = 'action_scheduler_migration_status'; + + /** Migration status option value. */ + const STATUS_COMPLETE = 'complete'; + + /** Migration minimum required PHP version. */ + const MIN_PHP_VERSION = '5.5'; + + /** @var ActionScheduler_DataController */ + private static $instance; + + /** @var int */ + private static $sleep_time = 0; + + /** @var int */ + private static $free_ticks = 50; + + /** + * Get a flag indicating whether the migration environment dependencies are met. + * + * @return bool + */ + public static function dependencies_met() { + $php_support = version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' ); + return $php_support && apply_filters( 'action_scheduler_migration_dependencies_met', true ); + } + + /** + * Get a flag indicating whether the migration is complete. + * + * @return bool Whether the flag has been set marking the migration as complete + */ + public static function is_migration_complete() { + return get_option( self::STATUS_FLAG ) === self::STATUS_COMPLETE; + } + + /** + * Mark the migration as complete. + */ + public static function mark_migration_complete() { + update_option( self::STATUS_FLAG, self::STATUS_COMPLETE ); + } + + /** + * Unmark migration when a plugin is de-activated. Will not work in case of silent activation, for example in an update. + * We do this to mitigate the bug of lost actions which happens if there was an AS 2.x to AS 3.x migration in the past, but that plugin is now + * deactivated and the site was running on AS 2.x again. + */ + public static function mark_migration_incomplete() { + delete_option( self::STATUS_FLAG ); + } + + /** + * Set the action store class name. + * + * @param string $class Classname of the store class. + * + * @return string + */ + public static function set_store_class( $class ) { + return self::DATASTORE_CLASS; + } + + /** + * Set the action logger class name. + * + * @param string $class Classname of the logger class. + * + * @return string + */ + public static function set_logger_class( $class ) { + return self::LOGGER_CLASS; + } + + /** + * Set the sleep time in seconds. + * + * @param integer $sleep_time The number of seconds to pause before resuming operation. + */ + public static function set_sleep_time( $sleep_time ) { + self::$sleep_time = (int) $sleep_time; + } + + /** + * Set the tick count required for freeing memory. + * + * @param integer $free_ticks The number of ticks to free memory on. + */ + public static function set_free_ticks( $free_ticks ) { + self::$free_ticks = (int) $free_ticks; + } + + /** + * Free memory if conditions are met. + * + * @param int $ticks Current tick count. + */ + public static function maybe_free_memory( $ticks ) { + if ( self::$free_ticks && 0 === $ticks % self::$free_ticks ) { + self::free_memory(); + } + } + + /** + * Reduce memory footprint by clearing the database query and object caches. + */ + public static function free_memory() { + if ( 0 < self::$sleep_time ) { + /* translators: %d: amount of time */ + \WP_CLI::warning( sprintf( _n( 'Stopped the insanity for %d second', 'Stopped the insanity for %d seconds', self::$sleep_time, 'action-scheduler' ), self::$sleep_time ) ); + sleep( self::$sleep_time ); + } + + \WP_CLI::warning( __( 'Attempting to reduce used memory...', 'action-scheduler' ) ); + + /** + * @var $wpdb \wpdb + * @var $wp_object_cache \WP_Object_Cache + */ + global $wpdb, $wp_object_cache; + + $wpdb->queries = array(); + + if ( ! is_a( $wp_object_cache, 'WP_Object_Cache' ) ) { + return; + } + + $wp_object_cache->group_ops = array(); + $wp_object_cache->stats = array(); + $wp_object_cache->memcache_debug = array(); + $wp_object_cache->cache = array(); + + if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) { + call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important + } + } + + /** + * Connect to table datastores if migration is complete. + * Otherwise, proceed with the migration if the dependencies have been met. + */ + public static function init() { + if ( self::is_migration_complete() ) { + add_filter( 'action_scheduler_store_class', array( 'ActionScheduler_DataController', 'set_store_class' ), 100 ); + add_filter( 'action_scheduler_logger_class', array( 'ActionScheduler_DataController', 'set_logger_class' ), 100 ); + add_action( 'deactivate_plugin', array( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) ); + } elseif ( self::dependencies_met() ) { + Controller::init(); + } + + add_action( 'action_scheduler/progress_tick', array( 'ActionScheduler_DataController', 'maybe_free_memory' ) ); + } + + /** + * Singleton factory. + */ + public static function instance() { + if ( ! isset( self::$instance ) ) { + self::$instance = new static(); + } + + return self::$instance; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php new file mode 100644 index 0000000000..5e8743cae7 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php @@ -0,0 +1,76 @@ +<?php + +/** + * ActionScheduler DateTime class. + * + * This is a custom extension to DateTime that + */ +class ActionScheduler_DateTime extends DateTime { + + /** + * UTC offset. + * + * Only used when a timezone is not set. When a timezone string is + * used, this will be set to 0. + * + * @var int + */ + protected $utcOffset = 0; + + /** + * Get the unix timestamp of the current object. + * + * Missing in PHP 5.2 so just here so it can be supported consistently. + * + * @return int + */ + public function getTimestamp() { + return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); + } + + /** + * Set the UTC offset. + * + * This represents a fixed offset instead of a timezone setting. + * + * @param $offset + */ + public function setUtcOffset( $offset ) { + $this->utcOffset = intval( $offset ); + } + + /** + * Returns the timezone offset. + * + * @return int + * @link http://php.net/manual/en/datetime.getoffset.php + */ + public function getOffset() { + return $this->utcOffset ? $this->utcOffset : parent::getOffset(); + } + + /** + * Set the TimeZone associated with the DateTime + * + * @param DateTimeZone $timezone + * + * @return static + * @link http://php.net/manual/en/datetime.settimezone.php + */ + public function setTimezone( $timezone ) { + $this->utcOffset = 0; + parent::setTimezone( $timezone ); + + return $this; + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php new file mode 100644 index 0000000000..353d3c0993 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php @@ -0,0 +1,11 @@ +<?php + +/** + * ActionScheduler Exception Interface. + * + * Facilitates catching Exceptions unique to Action Scheduler. + * + * @package ActionScheduler + * @since %VERSION% + */ +interface ActionScheduler_Exception {} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_FatalErrorMonitor.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_FatalErrorMonitor.php new file mode 100644 index 0000000000..5fa67d681b --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_FatalErrorMonitor.php @@ -0,0 +1,55 @@ +<?php + +/** + * Class ActionScheduler_FatalErrorMonitor + */ +class ActionScheduler_FatalErrorMonitor { + /** @var ActionScheduler_ActionClaim */ + private $claim = NULL; + /** @var ActionScheduler_Store */ + private $store = NULL; + private $action_id = 0; + + public function __construct( ActionScheduler_Store $store ) { + $this->store = $store; + } + + public function attach( ActionScheduler_ActionClaim $claim ) { + $this->claim = $claim; + add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); + add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 ); + add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 ); + add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 ); + } + + public function detach() { + $this->claim = NULL; + $this->untrack_action(); + remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); + remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 ); + remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 ); + } + + public function track_current_action( $action_id ) { + $this->action_id = $action_id; + } + + public function untrack_action() { + $this->action_id = 0; + } + + public function handle_unexpected_shutdown() { + if ( $error = error_get_last() ) { + if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ) ) ) { + if ( !empty($this->action_id) ) { + $this->store->mark_failure( $this->action_id ); + do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error ); + } + } + $this->store->release_claim( $this->claim ); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_InvalidActionException.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_InvalidActionException.php new file mode 100644 index 0000000000..40b4559932 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_InvalidActionException.php @@ -0,0 +1,47 @@ +<?php + +/** + * InvalidAction Exception. + * + * Used for identifying actions that are invalid in some way. + * + * @package ActionScheduler + */ +class ActionScheduler_InvalidActionException extends \InvalidArgumentException implements ActionScheduler_Exception { + + /** + * Create a new exception when the action's schedule cannot be fetched. + * + * @param string $action_id The action ID with bad args. + * @return static + */ + public static function from_schedule( $action_id, $schedule ) { + $message = sprintf( + /* translators: 1: action ID 2: schedule */ + __( 'Action [%1$s] has an invalid schedule: %2$s', 'action-scheduler' ), + $action_id, + var_export( $schedule, true ) + ); + + return new static( $message ); + } + + /** + * Create a new exception when the action's args cannot be decoded to an array. + * + * @author Jeremy Pry + * + * @param string $action_id The action ID with bad args. + * @return static + */ + public static function from_decoding_args( $action_id, $args = array() ) { + $message = sprintf( + /* translators: 1: action ID 2: arguments */ + __( 'Action [%1$s] has invalid arguments. It cannot be JSON decoded to an array. $args = %2$s', 'action-scheduler' ), + $action_id, + var_export( $args, true ) + ); + + return new static( $message ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php new file mode 100644 index 0000000000..501c0da298 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php @@ -0,0 +1,643 @@ +<?php + +/** + * Implements the admin view of the actions. + * @codeCoverageIgnore + */ +class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable { + + /** + * The package name. + * + * @var string + */ + protected $package = 'action-scheduler'; + + /** + * Columns to show (name => label). + * + * @var array + */ + protected $columns = array(); + + /** + * Actions (name => label). + * + * @var array + */ + protected $row_actions = array(); + + /** + * The active data stores + * + * @var ActionScheduler_Store + */ + protected $store; + + /** + * A logger to use for getting action logs to display + * + * @var ActionScheduler_Logger + */ + protected $logger; + + /** + * A ActionScheduler_QueueRunner runner instance (or child class) + * + * @var ActionScheduler_QueueRunner + */ + protected $runner; + + /** + * Bulk actions. The key of the array is the method name of the implementation: + * + * bulk_<key>(array $ids, string $sql_in). + * + * See the comments in the parent class for further details + * + * @var array + */ + protected $bulk_actions = array(); + + /** + * Flag variable to render our notifications, if any, once. + * + * @var bool + */ + protected static $did_notification = false; + + /** + * Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days" + * + * @var array + */ + private static $time_periods; + + /** + * Sets the current data store object into `store->action` and initialises the object. + * + * @param ActionScheduler_Store $store + * @param ActionScheduler_Logger $logger + * @param ActionScheduler_QueueRunner $runner + */ + public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) { + + $this->store = $store; + $this->logger = $logger; + $this->runner = $runner; + + $this->table_header = __( 'Scheduled Actions', 'action-scheduler' ); + + $this->bulk_actions = array( + 'delete' => __( 'Delete', 'action-scheduler' ), + ); + + $this->columns = array( + 'hook' => __( 'Hook', 'action-scheduler' ), + 'status' => __( 'Status', 'action-scheduler' ), + 'args' => __( 'Arguments', 'action-scheduler' ), + 'group' => __( 'Group', 'action-scheduler' ), + 'recurrence' => __( 'Recurrence', 'action-scheduler' ), + 'schedule' => __( 'Scheduled Date', 'action-scheduler' ), + 'log_entries' => __( 'Log', 'action-scheduler' ), + ); + + $this->sort_by = array( + 'schedule', + 'hook', + 'group', + ); + + $this->search_by = array( + 'hook', + 'args', + 'claim_id', + ); + + $request_status = $this->get_request_status(); + + if ( empty( $request_status ) ) { + $this->sort_by[] = 'status'; + } elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) { + $this->columns += array( 'claim_id' => __( 'Claim ID', 'action-scheduler' ) ); + $this->sort_by[] = 'claim_id'; + } + + $this->row_actions = array( + 'hook' => array( + 'run' => array( + 'name' => __( 'Run', 'action-scheduler' ), + 'desc' => __( 'Process the action now as if it were run as part of a queue', 'action-scheduler' ), + ), + 'cancel' => array( + 'name' => __( 'Cancel', 'action-scheduler' ), + 'desc' => __( 'Cancel the action now to avoid it being run in future', 'action-scheduler' ), + 'class' => 'cancel trash', + ), + ), + ); + + self::$time_periods = array( + array( + 'seconds' => YEAR_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s year', '%s years', 'action-scheduler' ), + ), + array( + 'seconds' => MONTH_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s month', '%s months', 'action-scheduler' ), + ), + array( + 'seconds' => WEEK_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s week', '%s weeks', 'action-scheduler' ), + ), + array( + 'seconds' => DAY_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s day', '%s days', 'action-scheduler' ), + ), + array( + 'seconds' => HOUR_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s hour', '%s hours', 'action-scheduler' ), + ), + array( + 'seconds' => MINUTE_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s minute', '%s minutes', 'action-scheduler' ), + ), + array( + 'seconds' => 1, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s second', '%s seconds', 'action-scheduler' ), + ), + ); + + parent::__construct( + array( + 'singular' => 'action-scheduler', + 'plural' => 'action-scheduler', + 'ajax' => false, + ) + ); + + add_screen_option( + 'per_page', + array( + 'default' => $this->items_per_page, + ) + ); + + add_filter( 'set_screen_option_' . $this->get_per_page_option_name(), array( $this, 'set_items_per_page_option' ), 10, 3 ); + set_screen_options(); + } + + /** + * Handles setting the items_per_page option for this screen. + * + * @param mixed $status Default false (to skip saving the current option). + * @param string $option Screen option name. + * @param int $value Screen option value. + * @return int + */ + public function set_items_per_page_option( $status, $option, $value ) { + return $value; + } + /** + * Convert an interval of seconds into a two part human friendly string. + * + * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning + * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step + * further to display two degrees of accuracy. + * + * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ + * + * @param int $interval A interval in seconds. + * @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included. + * @return string A human friendly string representation of the interval. + */ + private static function human_interval( $interval, $periods_to_include = 2 ) { + + if ( $interval <= 0 ) { + return __( 'Now!', 'action-scheduler' ); + } + + $output = ''; + + for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) { + + $periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] ); + + if ( $periods_in_interval > 0 ) { + if ( ! empty( $output ) ) { + $output .= ' '; + } + $output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'action-scheduler' ), $periods_in_interval ); + $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds']; + $periods_included++; + } + } + + return $output; + } + + /** + * Returns the recurrence of an action or 'Non-repeating'. The output is human readable. + * + * @param ActionScheduler_Action $action + * + * @return string + */ + protected function get_recurrence( $action ) { + $schedule = $action->get_schedule(); + if ( $schedule->is_recurring() ) { + $recurrence = $schedule->get_recurrence(); + + if ( is_numeric( $recurrence ) ) { + /* translators: %s: time interval */ + return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence ) ); + } else { + return $recurrence; + } + } + + return __( 'Non-repeating', 'action-scheduler' ); + } + + /** + * Serializes the argument of an action to render it in a human friendly format. + * + * @param array $row The array representation of the current row of the table + * + * @return string + */ + public function column_args( array $row ) { + if ( empty( $row['args'] ) ) { + return apply_filters( 'action_scheduler_list_table_column_args', '', $row ); + } + + $row_html = '<ul>'; + foreach ( $row['args'] as $key => $value ) { + $row_html .= sprintf( '<li><code>%s => %s</code></li>', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) ); + } + $row_html .= '</ul>'; + + return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row ); + } + + /** + * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. + * + * @param array $row Action array. + * @return string + */ + public function column_log_entries( array $row ) { + + $log_entries_html = '<ol>'; + + $timezone = new DateTimezone( 'UTC' ); + + foreach ( $row['log_entries'] as $log_entry ) { + $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone ); + } + + $log_entries_html .= '</ol>'; + + return $log_entries_html; + } + + /** + * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. + * + * @param ActionScheduler_LogEntry $log_entry + * @param DateTimezone $timezone + * @return string + */ + protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) { + $date = $log_entry->get_date(); + $date->setTimezone( $timezone ); + return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) ); + } + + /** + * Only display row actions for pending actions. + * + * @param array $row Row to render + * @param string $column_name Current row + * + * @return string + */ + protected function maybe_render_actions( $row, $column_name ) { + if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) { + return parent::maybe_render_actions( $row, $column_name ); + } + + return ''; + } + + /** + * Renders admin notifications + * + * Notifications: + * 1. When the maximum number of tasks are being executed simultaneously. + * 2. Notifications when a task is manually executed. + * 3. Tables are missing. + */ + public function display_admin_notices() { + global $wpdb; + + if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) { + $table_list = array( + 'actionscheduler_actions', + 'actionscheduler_logs', + 'actionscheduler_groups', + 'actionscheduler_claims', + ); + + $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + foreach ( $table_list as $table_name ) { + if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) { + $this->admin_notices[] = array( + 'class' => 'error', + 'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'action-scheduler' ), + ); + $this->recreate_tables(); + parent::display_admin_notices(); + + return; + } + } + } + + if ( $this->runner->has_maximum_concurrent_batches() ) { + $claim_count = $this->store->get_claim_count(); + $this->admin_notices[] = array( + 'class' => 'updated', + 'message' => sprintf( + /* translators: %s: amount of claims */ + _n( + 'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.', + 'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.', + $claim_count, + 'action-scheduler' + ), + $claim_count + ), + ); + } elseif ( $this->store->has_pending_actions_due() ) { + + $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' ); + + // No lock set or lock expired + if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) { + $in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) ); + /* translators: %s: process URL */ + $async_request_message = sprintf( __( 'A new queue has begun processing. <a href="%s">View actions in-progress »</a>', 'action-scheduler' ), esc_url( $in_progress_url ) ); + } else { + /* translators: %d: seconds */ + $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'action-scheduler' ), $async_request_lock_expiration - time() ); + } + + $this->admin_notices[] = array( + 'class' => 'notice notice-info', + 'message' => $async_request_message, + ); + } + + $notification = get_transient( 'action_scheduler_admin_notice' ); + + if ( is_array( $notification ) ) { + delete_transient( 'action_scheduler_admin_notice' ); + + $action = $this->store->fetch_action( $notification['action_id'] ); + $action_hook_html = '<strong><code>' . $action->get_hook() . '</code></strong>'; + if ( 1 == $notification['success'] ) { + $class = 'updated'; + switch ( $notification['row_action_type'] ) { + case 'run' : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully executed action: %s', 'action-scheduler' ), $action_hook_html ); + break; + case 'cancel' : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'action-scheduler' ), $action_hook_html ); + break; + default : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'action-scheduler' ), $action_hook_html ); + break; + } + } else { + $class = 'error'; + /* translators: 1: action HTML 2: action ID 3: error message */ + $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'action-scheduler' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) ); + } + + $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification ); + + $this->admin_notices[] = array( + 'class' => $class, + 'message' => $action_message_html, + ); + } + + parent::display_admin_notices(); + } + + /** + * Prints the scheduled date in a human friendly format. + * + * @param array $row The array representation of the current row of the table + * + * @return string + */ + public function column_schedule( $row ) { + return $this->get_schedule_display_string( $row['schedule'] ); + } + + /** + * Get the scheduled date in a human friendly format. + * + * @param ActionScheduler_Schedule $schedule + * @return string + */ + protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) { + + $schedule_display_string = ''; + + if ( ! $schedule->get_date() ) { + return '0000-00-00 00:00:00'; + } + + $next_timestamp = $schedule->get_date()->getTimestamp(); + + $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' ); + $schedule_display_string .= '<br/>'; + + if ( gmdate( 'U' ) > $next_timestamp ) { + /* translators: %s: date interval */ + $schedule_display_string .= sprintf( __( ' (%s ago)', 'action-scheduler' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) ); + } else { + /* translators: %s: date interval */ + $schedule_display_string .= sprintf( __( ' (%s)', 'action-scheduler' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) ); + } + + return $schedule_display_string; + } + + /** + * Bulk delete + * + * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data + * properly validated by the callee and it will delete the actions without any extra validation. + * + * @param array $ids + * @param string $ids_sql Inherited and unused + */ + protected function bulk_delete( array $ids, $ids_sql ) { + foreach ( $ids as $id ) { + $this->store->delete_action( $id ); + } + } + + /** + * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their + * parameters are valid. + * + * @param int $action_id + */ + protected function row_action_cancel( $action_id ) { + $this->process_row_action( $action_id, 'cancel' ); + } + + /** + * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their + * parameters are valid. + * + * @param int $action_id + */ + protected function row_action_run( $action_id ) { + $this->process_row_action( $action_id, 'run' ); + } + + /** + * Force the data store schema updates. + */ + protected function recreate_tables() { + if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) { + $store = $this->store; + } else { + $store = new ActionScheduler_HybridStore(); + } + add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 ); + + $store_schema = new ActionScheduler_StoreSchema(); + $logger_schema = new ActionScheduler_LoggerSchema(); + $store_schema->register_tables( true ); + $logger_schema->register_tables( true ); + + remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 ); + } + /** + * Implements the logic behind processing an action once an action link is clicked on the list table. + * + * @param int $action_id + * @param string $row_action_type The type of action to perform on the action. + */ + protected function process_row_action( $action_id, $row_action_type ) { + try { + switch ( $row_action_type ) { + case 'run' : + $this->runner->process_action( $action_id, 'Admin List Table' ); + break; + case 'cancel' : + $this->store->cancel_action( $action_id ); + break; + } + $success = 1; + $error_message = ''; + } catch ( Exception $e ) { + $success = 0; + $error_message = $e->getMessage(); + } + + set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 ); + } + + /** + * {@inheritDoc} + */ + public function prepare_items() { + $this->prepare_column_headers(); + + $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); + + $query = array( + 'per_page' => $per_page, + 'offset' => $this->get_items_offset(), + 'status' => $this->get_request_status(), + 'orderby' => $this->get_request_orderby(), + 'order' => $this->get_request_order(), + 'search' => $this->get_request_search_query(), + ); + + $this->items = array(); + + $total_items = $this->store->query_actions( $query, 'count' ); + + $status_labels = $this->store->get_status_labels(); + + foreach ( $this->store->query_actions( $query ) as $action_id ) { + try { + $action = $this->store->fetch_action( $action_id ); + } catch ( Exception $e ) { + continue; + } + if ( is_a( $action, 'ActionScheduler_NullAction' ) ) { + continue; + } + $this->items[ $action_id ] = array( + 'ID' => $action_id, + 'hook' => $action->get_hook(), + 'status_name' => $this->store->get_status( $action_id ), + 'status' => $status_labels[ $this->store->get_status( $action_id ) ], + 'args' => $action->get_args(), + 'group' => $action->get_group(), + 'log_entries' => $this->logger->get_logs( $action_id ), + 'claim_id' => $this->store->get_claim_id( $action_id ), + 'recurrence' => $this->get_recurrence( $action ), + 'schedule' => $action->get_schedule(), + ); + } + + $this->set_pagination_args( array( + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ) ); + } + + /** + * Prints the available statuses so the user can click to filter. + */ + protected function display_filter_by_status() { + $this->status_counts = $this->store->action_counts(); + parent::display_filter_by_status(); + } + + /** + * Get the text to display in the search box on the list table. + */ + protected function get_search_box_button_text() { + return __( 'Search hook, args and claim ID', 'action-scheduler' ); + } + + /** + * {@inheritDoc} + */ + protected function get_per_page_option_name() { + return str_replace( '-', '_', $this->screen->id ) . '_per_page'; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_LogEntry.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_LogEntry.php new file mode 100644 index 0000000000..649636debf --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_LogEntry.php @@ -0,0 +1,67 @@ +<?php + +/** + * Class ActionScheduler_LogEntry + */ +class ActionScheduler_LogEntry { + + /** + * @var int $action_id + */ + protected $action_id = ''; + + /** + * @var string $message + */ + protected $message = ''; + + /** + * @var Datetime $date + */ + protected $date; + + /** + * Constructor + * + * @param mixed $action_id Action ID + * @param string $message Message + * @param Datetime $date Datetime object with the time when this log entry was created. If this parameter is + * not provided a new Datetime object (with current time) will be created. + */ + public function __construct( $action_id, $message, $date = null ) { + + /* + * ActionScheduler_wpCommentLogger::get_entry() previously passed a 3rd param of $comment->comment_type + * to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin + * hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method, + * goodness knows why, so we need to guard against that here instead of using a DateTime type declaration + * for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE. + */ + if ( null !== $date && ! is_a( $date, 'DateTime' ) ) { + _doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' ); + $date = null; + } + + $this->action_id = $action_id; + $this->message = $message; + $this->date = $date ? $date : new Datetime; + } + + /** + * Returns the date when this log entry was created + * + * @return Datetime + */ + public function get_date() { + return $this->date; + } + + public function get_action_id() { + return $this->action_id; + } + + public function get_message() { + return $this->message; + } +} + diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_NullLogEntry.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_NullLogEntry.php new file mode 100644 index 0000000000..6f8f218aab --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_NullLogEntry.php @@ -0,0 +1,11 @@ +<?php + +/** + * Class ActionScheduler_NullLogEntry + */ +class ActionScheduler_NullLogEntry extends ActionScheduler_LogEntry { + public function __construct( $action_id = '', $message = '' ) { + // nothing to see here + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php new file mode 100644 index 0000000000..4bc9a3fc26 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php @@ -0,0 +1,49 @@ +<?php + +/** + * Provide a way to set simple transient locks to block behaviour + * for up-to a given duration. + * + * Class ActionScheduler_OptionLock + * @since 3.0.0 + */ +class ActionScheduler_OptionLock extends ActionScheduler_Lock { + + /** + * Set a lock using options for a given amount of time (60 seconds by default). + * + * Using an autoloaded option avoids running database queries or other resource intensive tasks + * on frequently triggered hooks, like 'init' or 'shutdown'. + * + * For example, ActionScheduler_QueueRunner->maybe_dispatch_async_request() uses a lock to avoid + * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', + * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() + * to find the current number of claims in the database. + * + * @param string $lock_type A string to identify different lock types. + * @bool True if lock value has changed, false if not or if set failed. + */ + public function set( $lock_type ) { + return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) ); + } + + /** + * If a lock is set, return the timestamp it was set to expiry. + * + * @param string $lock_type A string to identify different lock types. + * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. + */ + public function get_expiration( $lock_type ) { + return get_option( $this->get_key( $lock_type ) ); + } + + /** + * Get the key to use for storing the lock in the transient + * + * @param string $lock_type A string to identify different lock types. + * @return string + */ + protected function get_key( $lock_type ) { + return sprintf( 'action_scheduler_lock_%s', $lock_type ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php new file mode 100644 index 0000000000..49cd44bb2a --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php @@ -0,0 +1,158 @@ +<?php + +/** + * Class ActionScheduler_QueueCleaner + */ +class ActionScheduler_QueueCleaner { + + /** @var int */ + protected $batch_size; + + /** @var ActionScheduler_Store */ + private $store = null; + + /** + * 31 days in seconds. + * + * @var int + */ + private $month_in_seconds = 2678400; + + /** + * ActionScheduler_QueueCleaner constructor. + * + * @param ActionScheduler_Store $store The store instance. + * @param int $batch_size The batch size. + */ + public function __construct( ActionScheduler_Store $store = null, $batch_size = 20 ) { + $this->store = $store ? $store : ActionScheduler_Store::instance(); + $this->batch_size = $batch_size; + } + + public function delete_old_actions() { + $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); + $cutoff = as_get_datetime_object($lifespan.' seconds ago'); + + $statuses_to_purge = array( + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ); + + foreach ( $statuses_to_purge as $status ) { + $actions_to_delete = $this->store->query_actions( array( + 'status' => $status, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + } catch ( Exception $e ) { + + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @since 2.0.0 + * + * @param int $action_id The scheduled actions ID in the data store + * @param Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + */ + do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) ); + } + } + } + } + + /** + * Unclaim pending actions that have not been run within a given time limit. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes). + */ + public function reset_timeouts( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit ); + if ( $timeout < 0 ) { + return; + } + $cutoff = as_get_datetime_object($timeout.' seconds ago'); + $actions_to_reset = $this->store->query_actions( array( + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'claimed' => true, + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_reset as $action_id ) { + $this->store->unclaim_action( $action_id ); + do_action( 'action_scheduler_reset_action', $action_id ); + } + } + + /** + * Mark actions that have been running for more than a given time limit as failed, based on + * the assumption some uncatachable and unloggable fatal error occurred during processing. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes). + */ + public function mark_failures( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit ); + if ( $timeout < 0 ) { + return; + } + $cutoff = as_get_datetime_object($timeout.' seconds ago'); + $actions_to_reset = $this->store->query_actions( array( + 'status' => ActionScheduler_Store::STATUS_RUNNING, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_reset as $action_id ) { + $this->store->mark_failure( $action_id ); + do_action( 'action_scheduler_failed_action', $action_id, $timeout ); + } + } + + /** + * Do all of the cleaning actions. + * + * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes). + * @author Jeremy Pry + */ + public function clean( $time_limit = 300 ) { + $this->delete_old_actions(); + $this->reset_timeouts( $time_limit ); + $this->mark_failures( $time_limit ); + } + + /** + * Get the batch size for cleaning the queue. + * + * @author Jeremy Pry + * @return int + */ + protected function get_batch_size() { + /** + * Filter the batch size when cleaning the queue. + * + * @param int $batch_size The number of actions to clean in one batch. + */ + return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php new file mode 100644 index 0000000000..cd76807eef --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php @@ -0,0 +1,197 @@ +<?php + +/** + * Class ActionScheduler_QueueRunner + */ +class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { + const WP_CRON_HOOK = 'action_scheduler_run_queue'; + + const WP_CRON_SCHEDULE = 'every_minute'; + + /** @var ActionScheduler_AsyncRequest_QueueRunner */ + protected $async_request; + + /** @var ActionScheduler_QueueRunner */ + private static $runner = null; + + /** + * @return ActionScheduler_QueueRunner + * @codeCoverageIgnore + */ + public static function instance() { + if ( empty(self::$runner) ) { + $class = apply_filters('action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner'); + self::$runner = new $class(); + } + return self::$runner; + } + + /** + * ActionScheduler_QueueRunner constructor. + * + * @param ActionScheduler_Store $store + * @param ActionScheduler_FatalErrorMonitor $monitor + * @param ActionScheduler_QueueCleaner $cleaner + */ + public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null, ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) { + parent::__construct( $store, $monitor, $cleaner ); + + if ( is_null( $async_request ) ) { + $async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store ); + } + + $this->async_request = $async_request; + } + + /** + * @codeCoverageIgnore + */ + public function init() { + + add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); + + // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param + $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); + if ( $next_timestamp ) { + wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); + } + + $cron_context = array( 'WP Cron' ); + + if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { + $schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); + wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); + } + + add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) ); + $this->hook_dispatch_async_request(); + } + + /** + * Hook check for dispatching an async request. + */ + public function hook_dispatch_async_request() { + add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); + } + + /** + * Unhook check for dispatching an async request. + */ + public function unhook_dispatch_async_request() { + remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); + } + + /** + * Check if we should dispatch an async request to process actions. + * + * This method is attached to 'shutdown', so is called frequently. To avoid slowing down + * the site, it mitigates the work performed in each request by: + * 1. checking if it's in the admin context and then + * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) + * 3. haven't exceeded the number of allowed batches. + * + * The order of these checks is important, because they run from a check on a value: + * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant + * 2. in memory - transients use autoloaded options by default + * 3. from a database query - has_maximum_concurrent_batches() run the query + * $this->store->get_claim_count() to find the current number of claims in the DB. + * + * If all of these conditions are met, then we request an async runner check whether it + * should dispatch a request to process pending actions. + */ + public function maybe_dispatch_async_request() { + if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { + // Only start an async queue at most once every 60 seconds + ActionScheduler::lock()->set( 'async-request-runner' ); + $this->async_request->maybe_dispatch(); + } + } + + /** + * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' + * + * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 + * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context + * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, + * should set a context as the first parameter. For an example of this, refer to the code seen in + * @see ActionScheduler_AsyncRequest_QueueRunner::handle() + * + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + public function run( $context = 'WP Cron' ) { + ActionScheduler_Compatibility::raise_memory_limit(); + ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); + do_action( 'action_scheduler_before_process_queue' ); + $this->run_cleanup(); + $processed_actions = 0; + if ( false === $this->has_maximum_concurrent_batches() ) { + $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); + do { + $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); + $processed_actions += $processed_actions_in_batch; + } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory + } + + do_action( 'action_scheduler_after_process_queue' ); + return $processed_actions; + } + + /** + * Process a batch of actions pending in the queue. + * + * Actions are processed by claiming a set of pending actions then processing each one until either the batch + * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). + * + * @param int $size The maximum number of actions to process in the batch. + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + protected function do_batch( $size = 100, $context = '' ) { + $claim = $this->store->stake_claim($size); + $this->monitor->attach($claim); + $processed_actions = 0; + + foreach ( $claim->get_actions() as $action_id ) { + // bail if we lost the claim + if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) { + break; + } + $this->process_action( $action_id, $context ); + $processed_actions++; + + if ( $this->batch_limits_exceeded( $processed_actions ) ) { + break; + } + } + $this->store->release_claim($claim); + $this->monitor->detach(); + $this->clear_caches(); + return $processed_actions; + } + + /** + * Running large batches can eat up memory, as WP adds data to its object cache. + * + * If using a persistent object store, this has the side effect of flushing that + * as well, so this is disabled by default. To enable: + * + * add_filter( 'action_scheduler_queue_runner_flush_cache', '__return_true' ); + */ + protected function clear_caches() { + if ( ! wp_using_ext_object_cache() || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) { + wp_cache_flush(); + } + } + + public function add_wp_cron_schedule( $schedules ) { + $schedules['every_minute'] = array( + 'interval' => 60, // in seconds + 'display' => __( 'Every minute', 'action-scheduler' ), + ); + + return $schedules; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Versions.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Versions.php new file mode 100644 index 0000000000..915c2e6329 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_Versions.php @@ -0,0 +1,62 @@ +<?php + +/** + * Class ActionScheduler_Versions + */ +class ActionScheduler_Versions { + /** + * @var ActionScheduler_Versions + */ + private static $instance = NULL; + + private $versions = array(); + + public function register( $version_string, $initialization_callback ) { + if ( isset($this->versions[$version_string]) ) { + return FALSE; + } + $this->versions[$version_string] = $initialization_callback; + return TRUE; + } + + public function get_versions() { + return $this->versions; + } + + public function latest_version() { + $keys = array_keys($this->versions); + if ( empty($keys) ) { + return false; + } + uasort( $keys, 'version_compare' ); + return end($keys); + } + + public function latest_version_callback() { + $latest = $this->latest_version(); + if ( empty($latest) || !isset($this->versions[$latest]) ) { + return '__return_null'; + } + return $this->versions[$latest]; + } + + /** + * @return ActionScheduler_Versions + * @codeCoverageIgnore + */ + public static function instance() { + if ( empty(self::$instance) ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * @codeCoverageIgnore + */ + public static function initialize_latest_version() { + $self = self::instance(); + call_user_func($self->latest_version_callback()); + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php new file mode 100644 index 0000000000..1ba552c50b --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php @@ -0,0 +1,115 @@ +<?php + +/** + * Class ActionScheduler_WPCommentCleaner + * + * @since 3.0.0 + */ +class ActionScheduler_WPCommentCleaner { + + /** + * Post migration hook used to cleanup the WP comment table. + * + * @var string + */ + protected static $cleanup_hook = 'action_scheduler/cleanup_wp_comment_logs'; + + /** + * An instance of the ActionScheduler_wpCommentLogger class to interact with the comments table. + * + * This instance should only be used as an interface. It should not be initialized. + * + * @var ActionScheduler_wpCommentLogger + */ + protected static $wp_comment_logger = null; + + /** + * The key used to store the cached value of whether there are logs in the WP comment table. + * + * @var string + */ + protected static $has_logs_option_key = 'as_has_wp_comment_logs'; + + /** + * Initialize the class and attach callbacks. + */ + public static function init() { + if ( empty( self::$wp_comment_logger ) ) { + self::$wp_comment_logger = new ActionScheduler_wpCommentLogger(); + } + + add_action( self::$cleanup_hook, array( __CLASS__, 'delete_all_action_comments' ) ); + + // While there are orphaned logs left in the comments table, we need to attach the callbacks which filter comment counts. + add_action( 'pre_get_comments', array( self::$wp_comment_logger, 'filter_comment_queries' ), 10, 1 ); + add_action( 'wp_count_comments', array( self::$wp_comment_logger, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs + add_action( 'comment_feed_where', array( self::$wp_comment_logger, 'filter_comment_feed' ), 10, 2 ); + + // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen + add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) ); + add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) ); + } + + /** + * Determines if there are log entries in the wp comments table. + * + * Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup(). + * + * @return boolean Whether there are scheduled action comments in the comments table. + */ + public static function has_logs() { + return 'yes' === get_option( self::$has_logs_option_key ); + } + + /** + * Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled. + * Attached to the migration complete hook 'action_scheduler/migration_complete'. + */ + public static function maybe_schedule_cleanup() { + if ( (bool) get_comments( array( 'type' => ActionScheduler_wpCommentLogger::TYPE, 'number' => 1, 'fields' => 'ids' ) ) ) { + update_option( self::$has_logs_option_key, 'yes' ); + + if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) { + as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook ); + } + } + } + + /** + * Delete all action comments from the WP Comments table. + */ + public static function delete_all_action_comments() { + global $wpdb; + $wpdb->delete( $wpdb->comments, array( 'comment_type' => ActionScheduler_wpCommentLogger::TYPE, 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT ) ); + delete_option( self::$has_logs_option_key ); + } + + /** + * Registers admin notices about the orphaned action logs. + */ + public static function register_admin_notice() { + add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); + } + + /** + * Prints details about the orphaned action logs and includes information on where to learn more. + */ + public static function print_admin_notice() { + $next_cleanup_message = ''; + $next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook ); + + if ( $next_scheduled_cleanup_hook ) { + /* translators: %s: date interval */ + $next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'action-scheduler' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) ); + } + + $notice = sprintf( + /* translators: 1: next cleanup message 2: github issue URL */ + __( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s <a href="%2$s">Learn more »</a>', 'action-scheduler' ), + $next_cleanup_message, + 'https://github.com/woocommerce/action-scheduler/issues/368' + ); + + echo '<div class="notice notice-warning"><p>' . wp_kses_post( $notice ) . '</p></div>'; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/ActionScheduler_wcSystemStatus.php b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_wcSystemStatus.php new file mode 100644 index 0000000000..bca63e7158 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/ActionScheduler_wcSystemStatus.php @@ -0,0 +1,166 @@ +<?php + +/** + * Class ActionScheduler_wcSystemStatus + */ +class ActionScheduler_wcSystemStatus { + + /** + * The active data stores + * + * @var ActionScheduler_Store + */ + protected $store; + + /** + * Constructor method for ActionScheduler_wcSystemStatus. + * + * @param ActionScheduler_Store $store Active store object. + * + * @return void + */ + public function __construct( $store ) { + $this->store = $store; + } + + /** + * Display action data, including number of actions grouped by status and the oldest & newest action in each status. + * + * Helpful to identify issues, like a clogged queue. + */ + public function render() { + $action_counts = $this->store->action_counts(); + $status_labels = $this->store->get_status_labels(); + $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) ); + + $this->get_template( $status_labels, $action_counts, $oldest_and_newest ); + } + + /** + * Get oldest and newest scheduled dates for a given set of statuses. + * + * @param array $status_keys Set of statuses to find oldest & newest action for. + * @return array + */ + protected function get_oldest_and_newest( $status_keys ) { + + $oldest_and_newest = array(); + + foreach ( $status_keys as $status ) { + $oldest_and_newest[ $status ] = array( + 'oldest' => '–', + 'newest' => '–', + ); + + if ( 'in-progress' === $status ) { + continue; + } + + $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); + $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); + } + + return $oldest_and_newest; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param string $status Action status label/name string. + * @param string $date_type Oldest or Newest. + * @return DateTime + */ + protected function get_action_status_date( $status, $date_type = 'oldest' ) { + + $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; + + $action = $this->store->query_actions( + array( + 'claimed' => false, + 'status' => $status, + 'per_page' => 1, + 'order' => $order, + ) + ); + + if ( ! empty( $action ) ) { + $date_object = $this->store->get_date( $action[0] ); + $action_date = $date_object->format( 'Y-m-d H:i:s O' ); + } else { + $action_date = '–'; + } + + return $action_date; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param array $status_labels Set of statuses to find oldest & newest action for. + * @param array $action_counts Number of actions grouped by status. + * @param array $oldest_and_newest Date of the oldest and newest action with each status. + */ + protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) { + $as_version = ActionScheduler_Versions::instance()->latest_version(); + $as_datastore = get_class( ActionScheduler_Store::instance() ); + ?> + + <table class="wc_status_table widefat" cellspacing="0"> + <thead> + <tr> + <th colspan="5" data-export-label="Action Scheduler"><h2><?php esc_html_e( 'Action Scheduler', 'action-scheduler' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows details of Action Scheduler.', 'action-scheduler' ) ); ?></h2></th> + </tr> + <tr> + <td colspan="2" data-export-label="Version"><?php esc_html_e( 'Version:', 'action-scheduler' ); ?></td> + <td colspan="3"><?php echo esc_html( $as_version ); ?></td> + </tr> + <tr> + <td colspan="2" data-export-label="Data store"><?php esc_html_e( 'Data store:', 'action-scheduler' ); ?></td> + <td colspan="3"><?php echo esc_html( $as_datastore ); ?></td> + </tr> + <tr> + <td><strong><?php esc_html_e( 'Action Status', 'action-scheduler' ); ?></strong></td> + <td class="help"> </td> + <td><strong><?php esc_html_e( 'Count', 'action-scheduler' ); ?></strong></td> + <td><strong><?php esc_html_e( 'Oldest Scheduled Date', 'action-scheduler' ); ?></strong></td> + <td><strong><?php esc_html_e( 'Newest Scheduled Date', 'action-scheduler' ); ?></strong></td> + </tr> + </thead> + <tbody> + <?php + foreach ( $action_counts as $status => $count ) { + // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column. + printf( + '<tr><td>%1$s</td><td> </td><td>%2$s<span style="display: none;">, Oldest: %3$s, Newest: %4$s</span></td><td>%3$s</td><td>%4$s</td></tr>', + esc_html( $status_labels[ $status ] ), + esc_html( number_format_i18n( $count ) ), + esc_html( $oldest_and_newest[ $status ]['oldest'] ), + esc_html( $oldest_and_newest[ $status ]['newest'] ) + ); + } + ?> + </tbody> + </table> + + <?php + } + + /** + * Is triggered when invoking inaccessible methods in an object context. + * + * @param string $name Name of method called. + * @param array $arguments Parameters to invoke the method with. + * + * @return mixed + * @link https://php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.methods + */ + public function __call( $name, $arguments ) { + switch ( $name ) { + case 'print': + _deprecated_function( __CLASS__ . '::print()', '2.2.4', __CLASS__ . '::render()' ); + return call_user_func_array( array( $this, 'render' ), $arguments ); + } + + return null; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php new file mode 100644 index 0000000000..c33de68672 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php @@ -0,0 +1,197 @@ +<?php + +use Action_Scheduler\WP_CLI\ProgressBar; + +/** + * WP CLI Queue runner. + * + * This class can only be called from within a WP CLI instance. + */ +class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRunner { + + /** @var array */ + protected $actions; + + /** @var ActionScheduler_ActionClaim */ + protected $claim; + + /** @var \cli\progress\Bar */ + protected $progress_bar; + + /** + * ActionScheduler_WPCLI_QueueRunner constructor. + * + * @param ActionScheduler_Store $store + * @param ActionScheduler_FatalErrorMonitor $monitor + * @param ActionScheduler_QueueCleaner $cleaner + * + * @throws Exception When this is not run within WP CLI + */ + public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { + if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { + /* translators: %s php class name */ + throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) ); + } + + parent::__construct( $store, $monitor, $cleaner ); + } + + /** + * Set up the Queue before processing. + * + * @author Jeremy Pry + * + * @param int $batch_size The batch size to process. + * @param array $hooks The hooks being used to filter the actions claimed in this batch. + * @param string $group The group of actions to claim with this batch. + * @param bool $force Whether to force running even with too many concurrent processes. + * + * @return int The number of actions that will be run. + * @throws \WP_CLI\ExitException When there are too many concurrent batches. + */ + public function setup( $batch_size, $hooks = array(), $group = '', $force = false ) { + $this->run_cleanup(); + $this->add_hooks(); + + // Check to make sure there aren't too many concurrent processes running. + if ( $this->has_maximum_concurrent_batches() ) { + if ( $force ) { + WP_CLI::warning( __( 'There are too many concurrent batches, but the run is forced to continue.', 'action-scheduler' ) ); + } else { + WP_CLI::error( __( 'There are too many concurrent batches.', 'action-scheduler' ) ); + } + } + + // Stake a claim and store it. + $this->claim = $this->store->stake_claim( $batch_size, null, $hooks, $group ); + $this->monitor->attach( $this->claim ); + $this->actions = $this->claim->get_actions(); + + return count( $this->actions ); + } + + /** + * Add our hooks to the appropriate actions. + * + * @author Jeremy Pry + */ + protected function add_hooks() { + add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) ); + add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 ); + } + + /** + * Set up the WP CLI progress bar. + * + * @author Jeremy Pry + */ + protected function setup_progress_bar() { + $count = count( $this->actions ); + $this->progress_bar = new ProgressBar( + /* translators: %d: amount of actions */ + sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ), + $count + ); + } + + /** + * Process actions in the queue. + * + * @author Jeremy Pry + * + * @param string $context Optional runner context. Default 'WP CLI'. + * + * @return int The number of actions processed. + */ + public function run( $context = 'WP CLI' ) { + do_action( 'action_scheduler_before_process_queue' ); + $this->setup_progress_bar(); + foreach ( $this->actions as $action_id ) { + // Error if we lost the claim. + if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $this->claim->get_id() ) ) ) { + WP_CLI::warning( __( 'The claim has been lost. Aborting current batch.', 'action-scheduler' ) ); + break; + } + + $this->process_action( $action_id, $context ); + $this->progress_bar->tick(); + } + + $completed = $this->progress_bar->current(); + $this->progress_bar->finish(); + $this->store->release_claim( $this->claim ); + do_action( 'action_scheduler_after_process_queue' ); + + return $completed; + } + + /** + * Handle WP CLI message when the action is starting. + * + * @author Jeremy Pry + * + * @param $action_id + */ + public function before_execute( $action_id ) { + /* translators: %s refers to the action ID */ + WP_CLI::log( sprintf( __( 'Started processing action %s', 'action-scheduler' ), $action_id ) ); + } + + /** + * Handle WP CLI message when the action has completed. + * + * @author Jeremy Pry + * + * @param int $action_id + * @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility. + */ + public function after_execute( $action_id, $action = null ) { + // backward compatibility + if ( null === $action ) { + $action = $this->store->fetch_action( $action_id ); + } + /* translators: 1: action ID 2: hook name */ + WP_CLI::log( sprintf( __( 'Completed processing action %1$s with hook: %2$s', 'action-scheduler' ), $action_id, $action->get_hook() ) ); + } + + /** + * Handle WP CLI message when the action has failed. + * + * @author Jeremy Pry + * + * @param int $action_id + * @param Exception $exception + * @throws \WP_CLI\ExitException With failure message. + */ + public function action_failed( $action_id, $exception ) { + WP_CLI::error( + /* translators: 1: action ID 2: exception message */ + sprintf( __( 'Error processing action %1$s: %2$s', 'action-scheduler' ), $action_id, $exception->getMessage() ), + false + ); + } + + /** + * Sleep and help avoid hitting memory limit + * + * @param int $sleep_time Amount of seconds to sleep + * @deprecated 3.0.0 + */ + protected function stop_the_insanity( $sleep_time = 0 ) { + _deprecated_function( 'ActionScheduler_WPCLI_QueueRunner::stop_the_insanity', '3.0.0', 'ActionScheduler_DataController::free_memory' ); + + ActionScheduler_DataController::free_memory(); + } + + /** + * Maybe trigger the stop_the_insanity() method to free up memory. + */ + protected function maybe_stop_the_insanity() { + // The value returned by progress_bar->current() might be padded. Remove padding, and convert to int. + $current_iteration = intval( trim( $this->progress_bar->current() ) ); + if ( 0 === $current_iteration % 50 ) { + $this->stop_the_insanity(); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php new file mode 100644 index 0000000000..6cf27d4240 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php @@ -0,0 +1,158 @@ +<?php + +/** + * Commands for Action Scheduler. + */ +class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command { + + /** + * Run the Action Scheduler + * + * ## OPTIONS + * + * [--batch-size=<size>] + * : The maximum number of actions to run. Defaults to 100. + * + * [--batches=<size>] + * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue being executed until all actions are complete. + * + * [--cleanup-batch-size=<size>] + * : The maximum number of actions to clean up. Defaults to the value of --batch-size. + * + * [--hooks=<hooks>] + * : Only run actions with the specified hook. Omitting this option runs actions with any hook. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=hook_one,hook_two,hook_three` + * + * [--group=<group>] + * : Only run actions from the specified group. Omitting this option runs actions from all groups. + * + * [--free-memory-on=<count>] + * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. + * + * [--pause=<seconds>] + * : The number of seconds to pause when freeing memory. Default no pause. + * + * [--force] + * : Whether to force execution despite the maximum number of concurrent processes being exceeded. + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * @throws \WP_CLI\ExitException When an error occurs. + * + * @subcommand run + */ + public function run( $args, $assoc_args ) { + // Handle passed arguments. + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); + $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); + $hooks = array_filter( array_map( 'trim', $hooks ) ); + $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); + $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); + + ActionScheduler_DataController::set_free_ticks( $free_on ); + ActionScheduler_DataController::set_sleep_time( $sleep ); + + $batches_completed = 0; + $actions_completed = 0; + $unlimited = $batches === 0; + + try { + // Custom queue cleaner instance. + $cleaner = new ActionScheduler_QueueCleaner( null, $clean ); + + // Get the queue runner instance + $runner = new ActionScheduler_WPCLI_QueueRunner( null, null, $cleaner ); + + // Determine how many tasks will be run in the first batch. + $total = $runner->setup( $batch, $hooks, $group, $force ); + + // Run actions for as long as possible. + while ( $total > 0 ) { + $this->print_total_actions( $total ); + $actions_completed += $runner->run(); + $batches_completed++; + + // Maybe set up tasks for the next batch. + $total = ( $unlimited || $batches_completed < $batches ) ? $runner->setup( $batch, $hooks, $group, $force ) : 0; + } + } catch ( Exception $e ) { + $this->print_error( $e ); + } + + $this->print_total_batches( $batches_completed ); + $this->print_success( $actions_completed ); + } + + /** + * Print WP CLI message about how many actions are about to be processed. + * + * @author Jeremy Pry + * + * @param int $total + */ + protected function print_total_actions( $total ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to how many scheduled taks were found to run */ + _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ), + number_format_i18n( $total ) + ) + ); + } + + /** + * Print WP CLI message about how many batches of actions were processed. + * + * @author Jeremy Pry + * + * @param int $batches_completed + */ + protected function print_total_batches( $batches_completed ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to the total number of batches executed */ + _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ), + number_format_i18n( $batches_completed ) + ) + ); + } + + /** + * Convert an exception into a WP CLI error. + * + * @author Jeremy Pry + * + * @param Exception $e The error object. + * + * @throws \WP_CLI\ExitException + */ + protected function print_error( Exception $e ) { + WP_CLI::error( + sprintf( + /* translators: %s refers to the exception error message */ + __( 'There was an error running the action scheduler: %s', 'action-scheduler' ), + $e->getMessage() + ) + ); + } + + /** + * Print a success message with the number of completed actions. + * + * @author Jeremy Pry + * + * @param int $actions_completed + */ + protected function print_success( $actions_completed ) { + WP_CLI::success( + sprintf( + /* translators: %d refers to the total number of taskes completed */ + _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ), + number_format_i18n( $actions_completed ) + ) + ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/WP_CLI/Migration_Command.php b/vendor/woocommerce/action-scheduler/classes/WP_CLI/Migration_Command.php new file mode 100644 index 0000000000..066697e4e0 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/WP_CLI/Migration_Command.php @@ -0,0 +1,148 @@ +<?php + + +namespace Action_Scheduler\WP_CLI; + +use Action_Scheduler\Migration\Config; +use Action_Scheduler\Migration\Runner; +use Action_Scheduler\Migration\Scheduler; +use Action_Scheduler\Migration\Controller; +use WP_CLI; +use WP_CLI_Command; + +/** + * Class Migration_Command + * + * @package Action_Scheduler\WP_CLI + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class Migration_Command extends WP_CLI_Command { + + /** @var int */ + private $total_processed = 0; + + /** + * Register the command with WP-CLI + */ + public function register() { + if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { + return; + } + + WP_CLI::add_command( 'action-scheduler migrate', [ $this, 'migrate' ], [ + 'shortdesc' => 'Migrates actions to the DB tables store', + 'synopsis' => [ + [ + 'type' => 'assoc', + 'name' => 'batch-size', + 'optional' => true, + 'default' => 100, + 'description' => 'The number of actions to process in each batch', + ], + [ + 'type' => 'assoc', + 'name' => 'free-memory-on', + 'optional' => true, + 'default' => 50, + 'description' => 'The number of actions to process between freeing memory. 0 disables freeing memory', + ], + [ + 'type' => 'assoc', + 'name' => 'pause', + 'optional' => true, + 'default' => 0, + 'description' => 'The number of seconds to pause when freeing memory', + ], + [ + 'type' => 'flag', + 'name' => 'dry-run', + 'optional' => true, + 'description' => 'Reports on the actions that would have been migrated, but does not change any data', + ], + ], + ] ); + } + + /** + * Process the data migration. + * + * @param array $positional_args Required for WP CLI. Not used in migration. + * @param array $assoc_args Optional arguments. + * + * @return void + */ + public function migrate( $positional_args, $assoc_args ) { + $this->init_logging(); + + $config = $this->get_migration_config( $assoc_args ); + $runner = new Runner( $config ); + $runner->init_destination(); + + $batch_size = isset( $assoc_args[ 'batch-size' ] ) ? (int) $assoc_args[ 'batch-size' ] : 100; + $free_on = isset( $assoc_args[ 'free-memory-on' ] ) ? (int) $assoc_args[ 'free-memory-on' ] : 50; + $sleep = isset( $assoc_args[ 'pause' ] ) ? (int) $assoc_args[ 'pause' ] : 0; + \ActionScheduler_DataController::set_free_ticks( $free_on ); + \ActionScheduler_DataController::set_sleep_time( $sleep ); + + do { + $actions_processed = $runner->run( $batch_size ); + $this->total_processed += $actions_processed; + } while ( $actions_processed > 0 ); + + if ( ! $config->get_dry_run() ) { + // let the scheduler know that there's nothing left to do + $scheduler = new Scheduler(); + $scheduler->mark_complete(); + } + + WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) ); + } + + /** + * Build the config object used to create the Runner + * + * @param array $args Optional arguments. + * + * @return ActionScheduler\Migration\Config + */ + private function get_migration_config( $args ) { + $args = wp_parse_args( $args, [ + 'dry-run' => false, + ] ); + + $config = Controller::instance()->get_migration_config_object(); + $config->set_dry_run( ! empty( $args[ 'dry-run' ] ) ); + + return $config; + } + + /** + * Hook command line logging into migration actions. + */ + private function init_logging() { + add_action( 'action_scheduler/migrate_action_dry_run', function ( $action_id ) { + WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/no_action_to_migrate', function ( $action_id ) { + WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migrate_action_failed', function ( $action_id ) { + WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migrate_action_incomplete', function ( $source_id, $destination_id ) { + WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) ); + }, 10, 2 ); + add_action( 'action_scheduler/migrated_action', function ( $source_id, $destination_id ) { + WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) ); + }, 10, 2 ); + add_action( 'action_scheduler/migration_batch_starting', function ( $batch ) { + WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migration_batch_complete', function ( $batch ) { + WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) ); + }, 10, 1 ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/WP_CLI/ProgressBar.php b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ProgressBar.php new file mode 100644 index 0000000000..c86c74e838 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/WP_CLI/ProgressBar.php @@ -0,0 +1,119 @@ +<?php + +namespace Action_Scheduler\WP_CLI; + +/** + * WP_CLI progress bar for Action Scheduler. + */ + +/** + * Class ProgressBar + * + * @package Action_Scheduler\WP_CLI + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class ProgressBar { + + /** @var integer */ + protected $total_ticks; + + /** @var integer */ + protected $count; + + /** @var integer */ + protected $interval; + + /** @var string */ + protected $message; + + /** @var \cli\progress\Bar */ + protected $progress_bar; + + /** + * ProgressBar constructor. + * + * @param string $message Text to display before the progress bar. + * @param integer $count Total number of ticks to be performed. + * @param integer $interval Optional. The interval in milliseconds between updates. Default 100. + * + * @throws Exception When this is not run within WP CLI + */ + public function __construct( $message, $count, $interval = 100 ) { + if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { + /* translators: %s php class name */ + throw new \Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) ); + } + + $this->total_ticks = 0; + $this->message = $message; + $this->count = $count; + $this->interval = $interval; + } + + /** + * Increment the progress bar ticks. + */ + public function tick() { + if ( null === $this->progress_bar ) { + $this->setup_progress_bar(); + } + + $this->progress_bar->tick(); + $this->total_ticks++; + + do_action( 'action_scheduler/progress_tick', $this->total_ticks ); + } + + /** + * Get the progress bar tick count. + * + * @return int + */ + public function current() { + return $this->progress_bar ? $this->progress_bar->current() : 0; + } + + /** + * Finish the current progress bar. + */ + public function finish() { + if ( null !== $this->progress_bar ) { + $this->progress_bar->finish(); + } + + $this->progress_bar = null; + } + + /** + * Set the message used when creating the progress bar. + * + * @param string $message The message to be used when the next progress bar is created. + */ + public function set_message( $message ) { + $this->message = $message; + } + + /** + * Set the count for a new progress bar. + * + * @param integer $count The total number of ticks expected to complete. + */ + public function set_count( $count ) { + $this->count = $count; + $this->finish(); + } + + /** + * Set up the progress bar. + */ + protected function setup_progress_bar() { + $this->progress_bar = \WP_CLI\Utils\make_progress_bar( + $this->message, + $this->count, + $this->interval + ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php new file mode 100644 index 0000000000..a5a6161a22 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php @@ -0,0 +1,304 @@ +<?php + +use Action_Scheduler\WP_CLI\Migration_Command; +use Action_Scheduler\Migration\Controller; + +/** + * Class ActionScheduler + * @codeCoverageIgnore + */ +abstract class ActionScheduler { + private static $plugin_file = ''; + /** @var ActionScheduler_ActionFactory */ + private static $factory = NULL; + /** @var bool */ + private static $data_store_initialized = false; + + public static function factory() { + if ( !isset(self::$factory) ) { + self::$factory = new ActionScheduler_ActionFactory(); + } + return self::$factory; + } + + public static function store() { + return ActionScheduler_Store::instance(); + } + + public static function lock() { + return ActionScheduler_Lock::instance(); + } + + public static function logger() { + return ActionScheduler_Logger::instance(); + } + + public static function runner() { + return ActionScheduler_QueueRunner::instance(); + } + + public static function admin_view() { + return ActionScheduler_AdminView::instance(); + } + + /** + * Get the absolute system path to the plugin directory, or a file therein + * @static + * @param string $path + * @return string + */ + public static function plugin_path( $path ) { + $base = dirname(self::$plugin_file); + if ( $path ) { + return trailingslashit($base).$path; + } else { + return untrailingslashit($base); + } + } + + /** + * Get the absolute URL to the plugin directory, or a file therein + * @static + * @param string $path + * @return string + */ + public static function plugin_url( $path ) { + return plugins_url($path, self::$plugin_file); + } + + public static function autoload( $class ) { + $d = DIRECTORY_SEPARATOR; + $classes_dir = self::plugin_path( 'classes' . $d ); + $separator = strrpos( $class, '\\' ); + if ( false !== $separator ) { + if ( 0 !== strpos( $class, 'Action_Scheduler' ) ) { + return; + } + $class = substr( $class, $separator + 1 ); + } + + if ( 'Deprecated' === substr( $class, -10 ) ) { + $dir = self::plugin_path( 'deprecated' . $d ); + } elseif ( self::is_class_abstract( $class ) ) { + $dir = $classes_dir . 'abstracts' . $d; + } elseif ( self::is_class_migration( $class ) ) { + $dir = $classes_dir . 'migration' . $d; + } elseif ( 'Schedule' === substr( $class, -8 ) ) { + $dir = $classes_dir . 'schedules' . $d; + } elseif ( 'Action' === substr( $class, -6 ) ) { + $dir = $classes_dir . 'actions' . $d; + } elseif ( 'Schema' === substr( $class, -6 ) ) { + $dir = $classes_dir . 'schema' . $d; + } elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) { + $segments = explode( '_', $class ); + $type = isset( $segments[ 1 ] ) ? $segments[ 1 ] : ''; + + switch ( $type ) { + case 'WPCLI': + $dir = $classes_dir . 'WP_CLI' . $d; + break; + case 'DBLogger': + case 'DBStore': + case 'HybridStore': + case 'wpPostStore': + case 'wpCommentLogger': + $dir = $classes_dir . 'data-stores' . $d; + break; + default: + $dir = $classes_dir; + break; + } + } elseif ( self::is_class_cli( $class ) ) { + $dir = $classes_dir . 'WP_CLI' . $d; + } elseif ( strpos( $class, 'CronExpression' ) === 0 ) { + $dir = self::plugin_path( 'lib' . $d . 'cron-expression' . $d ); + } elseif ( strpos( $class, 'WP_Async_Request' ) === 0 ) { + $dir = self::plugin_path( 'lib' . $d ); + } else { + return; + } + + if ( file_exists( "{$dir}{$class}.php" ) ) { + include( "{$dir}{$class}.php" ); + return; + } + } + + /** + * Initialize the plugin + * + * @static + * @param string $plugin_file + */ + public static function init( $plugin_file ) { + self::$plugin_file = $plugin_file; + spl_autoload_register( array( __CLASS__, 'autoload' ) ); + + /** + * Fires in the early stages of Action Scheduler init hook. + */ + do_action( 'action_scheduler_pre_init' ); + + require_once( self::plugin_path( 'functions.php' ) ); + ActionScheduler_DataController::init(); + + $store = self::store(); + $logger = self::logger(); + $runner = self::runner(); + $admin_view = self::admin_view(); + + // Ensure initialization on plugin activation. + if ( ! did_action( 'init' ) ) { + add_action( 'init', array( $admin_view, 'init' ), 0, 0 ); // run before $store::init() + add_action( 'init', array( $store, 'init' ), 1, 0 ); + add_action( 'init', array( $logger, 'init' ), 1, 0 ); + add_action( 'init', array( $runner, 'init' ), 1, 0 ); + } else { + $admin_view->init(); + $store->init(); + $logger->init(); + $runner->init(); + } + + if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { + require_once( self::plugin_path( 'deprecated/functions.php' ) ); + } + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); + if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { + $command = new Migration_Command(); + $command->register(); + } + } + + self::$data_store_initialized = true; + + /** + * Handle WP comment cleanup after migration. + */ + if ( is_a( $logger, 'ActionScheduler_DBLogger' ) && ActionScheduler_DataController::is_migration_complete() && ActionScheduler_WPCommentCleaner::has_logs() ) { + ActionScheduler_WPCommentCleaner::init(); + } + + add_action( 'action_scheduler/migration_complete', 'ActionScheduler_WPCommentCleaner::maybe_schedule_cleanup' ); + } + + /** + * Check whether the AS data store has been initialized. + * + * @param string $function_name The name of the function being called. Optional. Default `null`. + * @return bool + */ + public static function is_initialized( $function_name = null ) { + if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { + $message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) ); + error_log( $message, E_WARNING ); + } + + return self::$data_store_initialized; + } + + /** + * Determine if the class is one of our abstract classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_abstract( $class ) { + static $abstracts = array( + 'ActionScheduler' => true, + 'ActionScheduler_Abstract_ListTable' => true, + 'ActionScheduler_Abstract_QueueRunner' => true, + 'ActionScheduler_Abstract_Schedule' => true, + 'ActionScheduler_Abstract_RecurringSchedule' => true, + 'ActionScheduler_Lock' => true, + 'ActionScheduler_Logger' => true, + 'ActionScheduler_Abstract_Schema' => true, + 'ActionScheduler_Store' => true, + 'ActionScheduler_TimezoneHelper' => true, + ); + + return isset( $abstracts[ $class ] ) && $abstracts[ $class ]; + } + + /** + * Determine if the class is one of our migration classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_migration( $class ) { + static $migration_segments = array( + 'ActionMigrator' => true, + 'BatchFetcher' => true, + 'DBStoreMigrator' => true, + 'DryRun' => true, + 'LogMigrator' => true, + 'Config' => true, + 'Controller' => true, + 'Runner' => true, + 'Scheduler' => true, + ); + + $segments = explode( '_', $class ); + $segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class; + + return isset( $migration_segments[ $segment ] ) && $migration_segments[ $segment ]; + } + + /** + * Determine if the class is one of our WP CLI classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_cli( $class ) { + static $cli_segments = array( + 'QueueRunner' => true, + 'Command' => true, + 'ProgressBar' => true, + ); + + $segments = explode( '_', $class ); + $segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class; + + return isset( $cli_segments[ $segment ] ) && $cli_segments[ $segment ]; + } + + final public function __clone() { + trigger_error("Singleton. No cloning allowed!", E_USER_ERROR); + } + + final public function __wakeup() { + trigger_error("Singleton. No serialization allowed!", E_USER_ERROR); + } + + final private function __construct() {} + + /** Deprecated **/ + + public static function get_datetime_object( $when = null, $timezone = 'UTC' ) { + _deprecated_function( __METHOD__, '2.0', 'wcs_add_months()' ); + return as_get_datetime_object( $when, $timezone ); + } + + /** + * Issue deprecated warning if an Action Scheduler function is called in the shutdown hook. + * + * @param string $function_name The name of the function being called. + * @deprecated 3.1.6. + */ + public static function check_shutdown_hook( $function_name ) { + _deprecated_function( __FUNCTION__, '3.1.6' ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php new file mode 100644 index 0000000000..4018599d06 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php @@ -0,0 +1,766 @@ +<?php + +if ( ! class_exists( 'WP_List_Table' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; +} + +/** + * Action Scheduler Abstract List Table class + * + * This abstract class enhances WP_List_Table making it ready to use. + * + * By extending this class we can focus on describing how our table looks like, + * which columns needs to be shown, filter, ordered by and more and forget about the details. + * + * This class supports: + * - Bulk actions + * - Search + * - Sortable columns + * - Automatic translations of the columns + * + * @codeCoverageIgnore + * @since 2.0.0 + */ +abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table { + + /** + * The table name + * + * @var string + */ + protected $table_name; + + /** + * Package name, used to get options from WP_List_Table::get_items_per_page. + * + * @var string + */ + protected $package; + + /** + * How many items do we render per page? + * + * @var int + */ + protected $items_per_page = 10; + + /** + * Enables search in this table listing. If this array + * is empty it means the listing is not searchable. + * + * @var array + */ + protected $search_by = array(); + + /** + * Columns to show in the table listing. It is a key => value pair. The + * key must much the table column name and the value is the label, which is + * automatically translated. + * + * @var array + */ + protected $columns = array(); + + /** + * Defines the row-actions. It expects an array where the key + * is the column name and the value is an array of actions. + * + * The array of actions are key => value, where key is the method name + * (with the prefix row_action_<key>) and the value is the label + * and title. + * + * @var array + */ + protected $row_actions = array(); + + /** + * The Primary key of our table + * + * @var string + */ + protected $ID = 'ID'; + + /** + * Enables sorting, it expects an array + * of columns (the column names are the values) + * + * @var array + */ + protected $sort_by = array(); + + /** + * The default sort order + * + * @var string + */ + protected $filter_by = array(); + + /** + * The status name => count combinations for this table's items. Used to display status filters. + * + * @var array + */ + protected $status_counts = array(); + + /** + * Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ). + * + * @var array + */ + protected $admin_notices = array(); + + /** + * Localised string displayed in the <h1> element above the able. + * + * @var string + */ + protected $table_header; + + /** + * Enables bulk actions. It must be an array where the key is the action name + * and the value is the label (which is translated automatically). It is important + * to notice that it will check that the method exists (`bulk_$name`) and will throw + * an exception if it does not exists. + * + * This class will automatically check if the current request has a bulk action, will do the + * validations and afterwards will execute the bulk method, with two arguments. The first argument + * is the array with primary keys, the second argument is a string with a list of the primary keys, + * escaped and ready to use (with `IN`). + * + * @var array + */ + protected $bulk_actions = array(); + + /** + * Makes translation easier, it basically just wraps + * `_x` with some default (the package name). + * + * @param string $text The new text to translate. + * @param string $context The context of the text. + * @return string|void The translated text. + * + * @deprecated 3.0.0 Use `_x()` instead. + */ + protected function translate( $text, $context = '' ) { + return $text; + } + + /** + * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It + * also validates that the bulk method handler exists. It throws an exception because + * this is a library meant for developers and missing a bulk method is a development-time error. + * + * @return array + * + * @throws RuntimeException Throws RuntimeException when the bulk action does not have a callback method. + */ + protected function get_bulk_actions() { + $actions = array(); + + foreach ( $this->bulk_actions as $action => $label ) { + if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) { + throw new RuntimeException( "The bulk action $action does not have a callback method" ); + } + + $actions[ $action ] = $label; + } + + return $actions; + } + + /** + * Checks if the current request has a bulk action. If that is the case it will validate and will + * execute the bulk method handler. Regardless if the action is valid or not it will redirect to + * the previous page removing the current arguments that makes this request a bulk action. + */ + protected function process_bulk_action() { + global $wpdb; + // Detect when a bulk action is being triggered. + $action = $this->current_action(); + if ( ! $action ) { + return; + } + + check_admin_referer( 'bulk-' . $this->_args['plural'] ); + + $method = 'bulk_' . $action; + if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) { + $ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')'; + $id = array_map( 'absint', $_GET['ID'] ); + $this->$method( $id, $wpdb->prepare( $ids_sql, $id ) ); //phpcs:ignore WordPress.DB.PreparedSQL + } + + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + wp_safe_redirect( + remove_query_arg( + array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ), + esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) + ) + ); + exit; + } + } + + /** + * Default code for deleting entries. + * validated already by process_bulk_action() + * + * @param array $ids ids of the items to delete. + * @param string $ids_sql the sql for the ids. + * @return void + */ + protected function bulk_delete( array $ids, $ids_sql ) { + $store = ActionScheduler::store(); + foreach ( $ids as $action_id ) { + $store->delete( $action_id ); + } + } + + /** + * Prepares the _column_headers property which is used by WP_Table_List at rendering. + * It merges the columns and the sortable columns. + */ + protected function prepare_column_headers() { + $this->_column_headers = array( + $this->get_columns(), + get_hidden_columns( $this->screen ), + $this->get_sortable_columns(), + ); + } + + /** + * Reads $this->sort_by and returns the columns name in a format that WP_Table_List + * expects + */ + public function get_sortable_columns() { + $sort_by = array(); + foreach ( $this->sort_by as $column ) { + $sort_by[ $column ] = array( $column, true ); + } + return $sort_by; + } + + /** + * Returns the columns names for rendering. It adds a checkbox for selecting everything + * as the first column + */ + public function get_columns() { + $columns = array_merge( + array( 'cb' => '<input type="checkbox" />' ), + $this->columns + ); + + return $columns; + } + + /** + * Get prepared LIMIT clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared LIMIT clause for items query. + */ + protected function get_items_query_limit() { + global $wpdb; + + $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); + return $wpdb->prepare( 'LIMIT %d', $per_page ); + } + + /** + * Returns the number of items to offset/skip for this current view. + * + * @return int + */ + protected function get_items_offset() { + $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); + $current_page = $this->get_pagenum(); + if ( 1 < $current_page ) { + $offset = $per_page * ( $current_page - 1 ); + } else { + $offset = 0; + } + + return $offset; + } + + /** + * Get prepared OFFSET clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared OFFSET clause for items query. + */ + protected function get_items_query_offset() { + global $wpdb; + + return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() ); + } + + /** + * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which + * columns are sortable. This requests validates the orderby $_GET parameter is a valid + * column and sortable. It will also use order (ASC|DESC) using DESC by default. + */ + protected function get_items_query_order() { + if ( empty( $this->sort_by ) ) { + return ''; + } + + $orderby = esc_sql( $this->get_request_orderby() ); + $order = esc_sql( $this->get_request_order() ); + + return "ORDER BY {$orderby} {$order}"; + } + + /** + * Return the sortable column specified for this request to order the results by, if any. + * + * @return string + */ + protected function get_request_orderby() { + + $valid_sortable_columns = array_values( $this->sort_by ); + + if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended + } else { + $orderby = $valid_sortable_columns[0]; + } + + return $orderby; + } + + /** + * Return the sortable column order specified for this request. + * + * @return string + */ + protected function get_request_order() { + + if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + $order = 'DESC'; + } else { + $order = 'ASC'; + } + + return $order; + } + + /** + * Return the status filter for this request, if any. + * + * @return string + */ + protected function get_request_status() { + $status = ( ! empty( $_GET['status'] ) ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended + return $status; + } + + /** + * Return the search filter for this request, if any. + * + * @return string + */ + protected function get_request_search_query() { + $search_query = ( ! empty( $_GET['s'] ) ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended + return $search_query; + } + + /** + * Process and return the columns name. This is meant for using with SQL, this means it + * always includes the primary key. + * + * @return array + */ + protected function get_table_columns() { + $columns = array_keys( $this->columns ); + if ( ! in_array( $this->ID, $columns, true ) ) { + $columns[] = $this->ID; + } + + return $columns; + } + + /** + * Check if the current request is doing a "full text" search. If that is the case + * prepares the SQL to search texts using LIKE. + * + * If the current request does not have any search or if this list table does not support + * that feature it will return an empty string. + * + * @return string + */ + protected function get_items_query_search() { + global $wpdb; + + if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + return ''; + } + + $search_string = sanitize_text_field( wp_unslash( $_GET['s'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended + + $filter = array(); + foreach ( $this->search_by as $column ) { + $wild = '%'; + $sql_like = $wild . $wpdb->esc_like( $search_string ) . $wild; + $filter[] = $wpdb->prepare( '`' . $column . '` LIKE %s', $sql_like ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.NotPrepared + } + return implode( ' OR ', $filter ); + } + + /** + * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting + * any data sent by the user it validates that it is a valid option. + */ + protected function get_items_query_filters() { + global $wpdb; + + if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + return ''; + } + + $filter = array(); + + foreach ( $this->filter_by as $column => $options ) { + if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + continue; + } + + $filter[] = $wpdb->prepare( "`$column` = %s", sanitize_text_field( wp_unslash( $_GET['filter_by'][ $column ] ) ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + return implode( ' AND ', $filter ); + + } + + /** + * Prepares the data to feed WP_Table_List. + * + * This has the core for selecting, sorting and filting data. To keep the code simple + * its logic is split among many methods (get_items_query_*). + * + * Beside populating the items this function will also count all the records that matches + * the filtering criteria and will do fill the pagination variables. + */ + public function prepare_items() { + global $wpdb; + + $this->process_bulk_action(); + + $this->process_row_actions(); + + if ( ! empty( $_REQUEST['_wp_http_referer'] && ! empty( $_SERVER['REQUEST_URI'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended + // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter + wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); + exit; + } + + $this->prepare_column_headers(); + + $limit = $this->get_items_query_limit(); + $offset = $this->get_items_query_offset(); + $order = $this->get_items_query_order(); + $where = array_filter( + array( + $this->get_items_query_search(), + $this->get_items_query_filters(), + ) + ); + $columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`'; + + if ( ! empty( $where ) ) { + $where = 'WHERE (' . implode( ') AND (', $where ) . ')'; + } else { + $where = ''; + } + + $sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}"; + + $this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + $query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}"; + $total_items = $wpdb->get_var( $query_count ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $per_page = $this->get_items_per_page( $this->get_per_page_option_name(), $this->items_per_page ); + $this->set_pagination_args( + array( + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ) + ); + } + + /** + * Display the table. + * + * @param string $which The name of the table. + */ + public function extra_tablenav( $which ) { + if ( ! $this->filter_by || 'top' !== $which ) { + return; + } + + echo '<div class="alignleft actions">'; + + foreach ( $this->filter_by as $id => $options ) { + $default = ! empty( $_GET['filter_by'][ $id ] ) ? sanitize_text_field( wp_unslash( $_GET['filter_by'][ $id ] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( empty( $options[ $default ] ) ) { + $default = ''; + } + + echo '<select name="filter_by[' . esc_attr( $id ) . ']" class="first" id="filter-by-' . esc_attr( $id ) . '">'; + + foreach ( $options as $value => $label ) { + echo '<option value="' . esc_attr( $value ) . '" ' . esc_html( $value === $default ? 'selected' : '' ) . '>' + . esc_html( $label ) + . '</option>'; + } + + echo '</select>'; + } + + submit_button( esc_html__( 'Filter', 'action-scheduler' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) ); + echo '</div>'; + } + + /** + * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns + * are serialized). This can be override in child classes for futher data transformation. + * + * @param array $items Items array. + */ + protected function set_items( array $items ) { + $this->items = array(); + foreach ( $items as $item ) { + $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item ); + } + } + + /** + * Renders the checkbox for each row, this is the first column and it is named ID regardless + * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper + * name transformation though using `$this->ID`. + * + * @param array $row The row to render. + */ + public function column_cb( $row ) { + return '<input name="ID[]" type="checkbox" value="' . esc_attr( $row[ $this->ID ] ) . '" />'; + } + + /** + * Renders the row-actions. + * + * This method renders the action menu, it reads the definition from the $row_actions property, + * and it checks that the row action method exists before rendering it. + * + * @param array $row Row to be rendered. + * @param string $column_name Column name. + * @return string + */ + protected function maybe_render_actions( $row, $column_name ) { + if ( empty( $this->row_actions[ $column_name ] ) ) { + return; + } + + $row_id = $row[ $this->ID ]; + + $actions = '<div class="row-actions">'; + $action_count = 0; + foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) { + + $action_count++; + + if ( ! method_exists( $this, 'row_action_' . $action_key ) ) { + continue; + } + + $action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg( + array( + 'row_action' => $action_key, + 'row_id' => $row_id, + 'nonce' => wp_create_nonce( $action_key . '::' . $row_id ), + ) + ); + $span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key; + $separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : ''; + + $actions .= sprintf( '<span class="%s">', esc_attr( $span_class ) ); + $actions .= sprintf( '<a href="%1$s" title="%2$s">%3$s</a>', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) ); + $actions .= sprintf( '%s</span>', $separator ); + } + $actions .= '</div>'; + return $actions; + } + + /** + * Process the bulk actions. + * + * @return void + */ + protected function process_row_actions() { + $parameters = array( 'row_action', 'row_id', 'nonce' ); + foreach ( $parameters as $parameter ) { + if ( empty( $_REQUEST[ $parameter ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + } + + $action = sanitize_text_field( wp_unslash( $_REQUEST['row_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + $row_id = sanitize_text_field( wp_unslash( $_REQUEST['row_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + $method = 'row_action_' . $action; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( wp_verify_nonce( $nonce, $action . '::' . $row_id ) && method_exists( $this, $method ) ) { + $this->$method( sanitize_text_field( wp_unslash( $row_id ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + wp_safe_redirect( + remove_query_arg( + array( 'row_id', 'row_action', 'nonce' ), + esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) + ) + ); + exit; + } + } + + /** + * Default column formatting, it will escape everythig for security. + * + * @param array $item The item array. + * @param string $column_name Column name to display. + * + * @return string + */ + public function column_default( $item, $column_name ) { + $column_html = esc_html( $item[ $column_name ] ); + $column_html .= $this->maybe_render_actions( $item, $column_name ); + return $column_html; + } + + /** + * Display the table heading and search query, if any + */ + protected function display_header() { + echo '<h1 class="wp-heading-inline">' . esc_attr( $this->table_header ) . '</h1>'; + if ( $this->get_request_search_query() ) { + /* translators: %s: search query */ + echo '<span class="subtitle">' . esc_attr( sprintf( __( 'Search results for "%s"', 'action-scheduler' ), $this->get_request_search_query() ) ) . '</span>'; + } + echo '<hr class="wp-header-end">'; + } + + /** + * Display the table heading and search query, if any + */ + protected function display_admin_notices() { + foreach ( $this->admin_notices as $notice ) { + echo '<div id="message" class="' . esc_attr( $notice['class'] ) . '">'; + echo ' <p>' . wp_kses_post( $notice['message'] ) . '</p>'; + echo '</div>'; + } + } + + /** + * Prints the available statuses so the user can click to filter. + */ + protected function display_filter_by_status() { + + $status_list_items = array(); + $request_status = $this->get_request_status(); + + // Helper to set 'all' filter when not set on status counts passed in. + if ( ! isset( $this->status_counts['all'] ) ) { + $this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts; + } + + foreach ( $this->status_counts as $status_name => $count ) { + + if ( 0 === $count ) { + continue; + } + + if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) { + $status_list_item = '<li class="%1$s"><strong>%3$s</strong> (%4$d)</li>'; + } else { + $status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>'; + } + + $status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name ); + $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); + $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) ); + } + + if ( $status_list_items ) { + echo '<ul class="subsubsub">'; + echo implode( " | \n", $status_list_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '</ul>'; + } + } + + /** + * Renders the table list, we override the original class to render the table inside a form + * and to render any needed HTML (like the search box). By doing so the callee of a function can simple + * forget about any extra HTML. + */ + protected function display_table() { + echo '<form id="' . esc_attr( $this->_args['plural'] ) . '-filter" method="get">'; + foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( '_' === $key[0] || 'paged' === $key || 'ID' === $key ) { + continue; + } + echo '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />'; + } + if ( ! empty( $this->search_by ) ) { + echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + parent::display(); + echo '</form>'; + } + + /** + * Process any pending actions. + */ + public function process_actions() { + $this->process_bulk_action(); + $this->process_row_actions(); + + if ( ! empty( $_REQUEST['_wp_http_referer'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter + wp_safe_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); + exit; + } + } + + /** + * Render the list table page, including header, notices, status filters and table. + */ + public function display_page() { + $this->prepare_items(); + + echo '<div class="wrap">'; + $this->display_header(); + $this->display_admin_notices(); + $this->display_filter_by_status(); + $this->display_table(); + echo '</div>'; + } + + /** + * Get the text to display in the search box on the list table. + */ + protected function get_search_box_placeholder() { + return esc_html__( 'Search', 'action-scheduler' ); + } + + /** + * Gets the screen per_page option name. + * + * @return string + */ + protected function get_per_page_option_name() { + return $this->package . '_items_per_page'; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php new file mode 100644 index 0000000000..82ecbc6796 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php @@ -0,0 +1,240 @@ +<?php + +/** + * Abstract class with common Queue Cleaner functionality. + */ +abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated { + + /** @var ActionScheduler_QueueCleaner */ + protected $cleaner; + + /** @var ActionScheduler_FatalErrorMonitor */ + protected $monitor; + + /** @var ActionScheduler_Store */ + protected $store; + + /** + * The created time. + * + * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running. + * For this reason it should be as close as possible to the PHP request start time. + * + * @var int + */ + private $created_time; + + /** + * ActionScheduler_Abstract_QueueRunner constructor. + * + * @param ActionScheduler_Store $store + * @param ActionScheduler_FatalErrorMonitor $monitor + * @param ActionScheduler_QueueCleaner $cleaner + */ + public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { + + $this->created_time = microtime( true ); + + $this->store = $store ? $store : ActionScheduler_Store::instance(); + $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store ); + $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store ); + } + + /** + * Process an individual action. + * + * @param int $action_id The action ID to process. + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + */ + public function process_action( $action_id, $context = '' ) { + try { + $valid_action = false; + do_action( 'action_scheduler_before_execute', $action_id, $context ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id, $context ); + return; + } + + $valid_action = true; + do_action( 'action_scheduler_begin_execute', $action_id, $context ); + + $action = $this->store->fetch_action( $action_id ); + $this->store->log_execution( $action_id ); + $action->execute(); + do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); + $this->store->mark_complete( $action_id ); + } catch ( Exception $e ) { + if ( $valid_action ) { + $this->store->mark_failure( $action_id ); + do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); + } else { + do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); + } + } + + if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { + $this->schedule_next_instance( $action, $action_id ); + } + } + + /** + * Schedule the next instance of the action if necessary. + * + * @param ActionScheduler_Action $action + * @param int $action_id + */ + protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) { + try { + ActionScheduler::factory()->repeat( $action ); + } catch ( Exception $e ) { + do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action ); + } + } + + /** + * Run the queue cleaner. + * + * @author Jeremy Pry + */ + protected function run_cleanup() { + $this->cleaner->clean( 10 * $this->get_time_limit() ); + } + + /** + * Get the number of concurrent batches a runner allows. + * + * @return int + */ + public function get_allowed_concurrent_batches() { + return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 ); + } + + /** + * Check if the number of allowed concurrent batches is met or exceeded. + * + * @return bool + */ + public function has_maximum_concurrent_batches() { + return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches(); + } + + /** + * Get the maximum number of seconds a batch can run for. + * + * @return int The number of seconds. + */ + protected function get_time_limit() { + + $time_limit = 30; + + // Apply deprecated filter from deprecated get_maximum_execution_time() method + if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { + _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); + $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit ); + } + + return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) ); + } + + /** + * Get the number of seconds the process has been running. + * + * @return int The number of seconds. + */ + protected function get_execution_time() { + $execution_time = microtime( true ) - $this->created_time; + + // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. + if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { + $resource_usages = getrusage(); + + if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { + $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); + } + } + + return $execution_time; + } + + /** + * Check if the host's max execution time is (likely) to be exceeded if processing more actions. + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function time_likely_to_be_exceeded( $processed_actions ) { + + $execution_time = $this->get_execution_time(); + $max_execution_time = $this->get_time_limit(); + $time_per_action = $execution_time / $processed_actions; + $estimated_time = $execution_time + ( $time_per_action * 3 ); + $likely_to_be_exceeded = $estimated_time > $max_execution_time; + + return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time ); + } + + /** + * Get memory limit + * + * Based on WP_Background_Process::get_memory_limit() + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce + } + + if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) { + // Unlimited, set to 32GB. + $memory_limit = '32G'; + } + + return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit ); + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% of the maximum WordPress memory. + * + * Based on WP_Background_Process::memory_exceeded() + * + * @return bool + */ + protected function memory_exceeded() { + + $memory_limit = $this->get_memory_limit() * 0.90; + $current_memory = memory_get_usage( true ); + $memory_exceeded = $current_memory >= $memory_limit; + + return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this ); + } + + /** + * See if the batch limits have been exceeded, which is when memory usage is almost at + * the maximum limit, or the time to process more actions will exceed the max time limit. + * + * Based on WC_Background_Process::batch_limits_exceeded() + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function batch_limits_exceeded( $processed_actions ) { + return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions ); + } + + /** + * Process actions in the queue. + * + * @author Jeremy Pry + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + abstract public function run( $context = '' ); +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php new file mode 100644 index 0000000000..131d4757d8 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php @@ -0,0 +1,102 @@ +<?php + +/** + * Class ActionScheduler_Abstract_RecurringSchedule + */ +abstract class ActionScheduler_Abstract_RecurringSchedule extends ActionScheduler_Abstract_Schedule { + + /** + * The date & time the first instance of this schedule was setup to run (which may not be this instance). + * + * Schedule objects are attached to an action object. Each schedule stores the run date for that + * object as the start date - @see $this->start - and logic to calculate the next run date after + * that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very + * first instance of this chain of schedules ran. + * + * @var DateTime + */ + private $first_date = NULL; + + /** + * Timestamp equivalent of @see $this->first_date + * + * @var int + */ + protected $first_timestamp = NULL; + + /** + * The recurrance between each time an action is run using this schedule. + * Used to calculate the start date & time. Can be a number of seconds, in the + * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the + * case of ActionScheduler_CronSchedule. Or something else. + * + * @var mixed + */ + protected $recurrence; + + /** + * @param DateTime $date The date & time to run the action. + * @param mixed $recurrence The data used to determine the schedule's recurrance. + * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. + */ + public function __construct( DateTime $date, $recurrence, DateTime $first = null ) { + parent::__construct( $date ); + $this->first_date = empty( $first ) ? $date : $first; + $this->recurrence = $recurrence; + } + + /** + * @return bool + */ + public function is_recurring() { + return true; + } + + /** + * Get the date & time of the first schedule in this recurring series. + * + * @return DateTime|null + */ + public function get_first_date() { + return clone $this->first_date; + } + + /** + * @return string + */ + public function get_recurrence() { + return $this->recurrence; + } + + /** + * For PHP 5.2 compat, since DateTime objects can't be serialized + * @return array + */ + public function __sleep() { + $sleep_params = parent::__sleep(); + $this->first_timestamp = $this->first_date->getTimestamp(); + return array_merge( $sleep_params, array( + 'first_timestamp', + 'recurrence' + ) ); + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in + * Action Scheduler 3.0.0, where properties and property names were aligned for better + * inheritance. To maintain backward compatibility with scheduled serialized and stored + * prior to 3.0, we need to correctly map the old property names. + */ + public function __wakeup() { + parent::__wakeup(); + if ( $this->first_timestamp > 0 ) { + $this->first_date = as_get_datetime_object( $this->first_timestamp ); + } else { + $this->first_date = $this->get_date(); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php new file mode 100644 index 0000000000..2631ef554f --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php @@ -0,0 +1,83 @@ +<?php + +/** + * Class ActionScheduler_Abstract_Schedule + */ +abstract class ActionScheduler_Abstract_Schedule extends ActionScheduler_Schedule_Deprecated { + + /** + * The date & time the schedule is set to run. + * + * @var DateTime + */ + private $scheduled_date = NULL; + + /** + * Timestamp equivalent of @see $this->scheduled_date + * + * @var int + */ + protected $scheduled_timestamp = NULL; + + /** + * @param DateTime $date The date & time to run the action. + */ + public function __construct( DateTime $date ) { + $this->scheduled_date = $date; + } + + /** + * Check if a schedule should recur. + * + * @return bool + */ + abstract public function is_recurring(); + + /** + * Calculate when the next instance of this schedule would run based on a given date & time. + * + * @param DateTime $after + * @return DateTime + */ + abstract protected function calculate_next( DateTime $after ); + + /** + * Get the next date & time when this schedule should run after a given date & time. + * + * @param DateTime $after + * @return DateTime|null + */ + public function get_next( DateTime $after ) { + $after = clone $after; + if ( $after > $this->scheduled_date ) { + $after = $this->calculate_next( $after ); + return $after; + } + return clone $this->scheduled_date; + } + + /** + * Get the date & time the schedule is set to run. + * + * @return DateTime|null + */ + public function get_date() { + return $this->scheduled_date; + } + + /** + * For PHP 5.2 compat, since DateTime objects can't be serialized + * @return array + */ + public function __sleep() { + $this->scheduled_timestamp = $this->scheduled_date->getTimestamp(); + return array( + 'scheduled_timestamp', + ); + } + + public function __wakeup() { + $this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp ); + unset( $this->scheduled_timestamp ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php new file mode 100644 index 0000000000..2334fda10e --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php @@ -0,0 +1,172 @@ +<?php + + +/** + * Class ActionScheduler_Abstract_Schema + * + * @package Action_Scheduler + * + * @codeCoverageIgnore + * + * Utility class for creating/updating custom tables + */ +abstract class ActionScheduler_Abstract_Schema { + + /** + * @var int Increment this value in derived class to trigger a schema update. + */ + protected $schema_version = 1; + + /** + * @var string Schema version stored in database. + */ + protected $db_version; + + /** + * @var array Names of tables that will be registered by this class. + */ + protected $tables = []; + + /** + * Can optionally be used by concrete classes to carry out additional initialization work + * as needed. + */ + public function init() {} + + /** + * Register tables with WordPress, and create them if needed. + * + * @param bool $force_update Optional. Default false. Use true to always run the schema update. + * + * @return void + */ + public function register_tables( $force_update = false ) { + global $wpdb; + + // make WP aware of our tables + foreach ( $this->tables as $table ) { + $wpdb->tables[] = $table; + $name = $this->get_full_table_name( $table ); + $wpdb->$table = $name; + } + + // create the tables + if ( $this->schema_update_required() || $force_update ) { + foreach ( $this->tables as $table ) { + /** + * Allow custom processing before updating a table schema. + * + * @param string $table Name of table being updated. + * @param string $db_version Existing version of the table being updated. + */ + do_action( 'action_scheduler_before_schema_update', $table, $this->db_version ); + $this->update_table( $table ); + } + $this->mark_schema_update_complete(); + } + } + + /** + * @param string $table The name of the table + * + * @return string The CREATE TABLE statement, suitable for passing to dbDelta + */ + abstract protected function get_table_definition( $table ); + + /** + * Determine if the database schema is out of date + * by comparing the integer found in $this->schema_version + * with the option set in the WordPress options table + * + * @return bool + */ + private function schema_update_required() { + $option_name = 'schema-' . static::class; + $this->db_version = get_option( $option_name, 0 ); + + // Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema + if ( 0 === $this->db_version ) { + + $plugin_option_name = 'schema-'; + + switch ( static::class ) { + case 'ActionScheduler_StoreSchema' : + $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; + break; + case 'ActionScheduler_LoggerSchema' : + $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; + break; + } + + $this->db_version = get_option( $plugin_option_name, 0 ); + + delete_option( $plugin_option_name ); + } + + return version_compare( $this->db_version, $this->schema_version, '<' ); + } + + /** + * Update the option in WordPress to indicate that + * our schema is now up to date + * + * @return void + */ + private function mark_schema_update_complete() { + $option_name = 'schema-' . static::class; + + // work around race conditions and ensure that our option updates + $value_to_save = (string) $this->schema_version . '.0.' . time(); + + update_option( $option_name, $value_to_save ); + } + + /** + * Update the schema for the given table + * + * @param string $table The name of the table to update + * + * @return void + */ + private function update_table( $table ) { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + $definition = $this->get_table_definition( $table ); + if ( $definition ) { + $updated = dbDelta( $definition ); + foreach ( $updated as $updated_table => $update_description ) { + if ( strpos( $update_description, 'Created table' ) === 0 ) { + do_action( 'action_scheduler/created_table', $updated_table, $table ); + } + } + } + } + + /** + * @param string $table + * + * @return string The full name of the table, including the + * table prefix for the current blog + */ + protected function get_full_table_name( $table ) { + return $GLOBALS[ 'wpdb' ]->prefix . $table; + } + + /** + * Confirms that all of the tables registered by this schema class have been created. + * + * @return bool + */ + public function tables_exist() { + global $wpdb; + + $existing_tables = $wpdb->get_col( 'SHOW TABLES' ); + $expected_tables = array_map( + function ( $table_name ) use ( $wpdb ) { + return $wpdb->prefix . $table_name; + }, + $this->tables + ); + + return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php new file mode 100644 index 0000000000..86e8528512 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php @@ -0,0 +1,62 @@ +<?php + +/** + * Abstract class for setting a basic lock to throttle some action. + * + * Class ActionScheduler_Lock + */ +abstract class ActionScheduler_Lock { + + /** @var ActionScheduler_Lock */ + private static $locker = NULL; + + /** @var int */ + protected static $lock_duration = MINUTE_IN_SECONDS; + + /** + * Check if a lock is set for a given lock type. + * + * @param string $lock_type A string to identify different lock types. + * @return bool + */ + public function is_locked( $lock_type ) { + return ( $this->get_expiration( $lock_type ) >= time() ); + } + + /** + * Set a lock. + * + * @param string $lock_type A string to identify different lock types. + * @return bool + */ + abstract public function set( $lock_type ); + + /** + * If a lock is set, return the timestamp it was set to expiry. + * + * @param string $lock_type A string to identify different lock types. + * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. + */ + abstract public function get_expiration( $lock_type ); + + /** + * Get the amount of time to set for a given lock. 60 seconds by default. + * + * @param string $lock_type A string to identify different lock types. + * @return int + */ + protected function get_duration( $lock_type ) { + return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type ); + } + + /** + * @return ActionScheduler_Lock + */ + public static function instance() { + if ( empty( self::$locker ) ) { + $class = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' ); + self::$locker = new $class(); + } + return self::$locker; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php new file mode 100644 index 0000000000..3e7252c555 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php @@ -0,0 +1,176 @@ +<?php + +/** + * Class ActionScheduler_Logger + * @codeCoverageIgnore + */ +abstract class ActionScheduler_Logger { + private static $logger = NULL; + + /** + * @return ActionScheduler_Logger + */ + public static function instance() { + if ( empty(self::$logger) ) { + $class = apply_filters('action_scheduler_logger_class', 'ActionScheduler_wpCommentLogger'); + self::$logger = new $class(); + } + return self::$logger; + } + + /** + * @param string $action_id + * @param string $message + * @param DateTime $date + * + * @return string The log entry ID + */ + abstract public function log( $action_id, $message, DateTime $date = NULL ); + + /** + * @param string $entry_id + * + * @return ActionScheduler_LogEntry + */ + abstract public function get_entry( $entry_id ); + + /** + * @param string $action_id + * + * @return ActionScheduler_LogEntry[] + */ + abstract public function get_logs( $action_id ); + + + /** + * @codeCoverageIgnore + */ + public function init() { + $this->hook_stored_action(); + add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 ); + add_action( 'action_scheduler_begin_execute', array( $this, 'log_started_action' ), 10, 2 ); + add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 3 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 3 ); + add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 ); + add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 ); + add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 ); + add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 2 ); + add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 2 ); + add_action( 'action_scheduler_failed_to_schedule_next_instance', array( $this, 'log_failed_schedule_next_instance' ), 10, 2 ); + add_action( 'action_scheduler_bulk_cancel_actions', array( $this, 'bulk_log_cancel_actions' ), 10, 1 ); + } + + public function hook_stored_action() { + add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); + } + + public function unhook_stored_action() { + remove_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); + } + + public function log_stored_action( $action_id ) { + $this->log( $action_id, __( 'action created', 'action-scheduler' ) ); + } + + public function log_canceled_action( $action_id ) { + $this->log( $action_id, __( 'action canceled', 'action-scheduler' ) ); + } + + public function log_started_action( $action_id, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action started via %s', 'action-scheduler' ), $context ); + } else { + $message = __( 'action started', 'action-scheduler' ); + } + $this->log( $action_id, $message ); + } + + public function log_completed_action( $action_id, $action = NULL, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action complete via %s', 'action-scheduler' ), $context ); + } else { + $message = __( 'action complete', 'action-scheduler' ); + } + $this->log( $action_id, $message ); + } + + public function log_failed_action( $action_id, Exception $exception, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: 1: context 2: exception message */ + $message = sprintf( __( 'action failed via %1$s: %2$s', 'action-scheduler' ), $context, $exception->getMessage() ); + } else { + /* translators: %s: exception message */ + $message = sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ); + } + $this->log( $action_id, $message ); + } + + public function log_timed_out_action( $action_id, $timeout ) { + /* translators: %s: amount of time */ + $this->log( $action_id, sprintf( __( 'action timed out after %s seconds', 'action-scheduler' ), $timeout ) ); + } + + public function log_unexpected_shutdown( $action_id, $error ) { + if ( ! empty( $error ) ) { + /* translators: 1: error message 2: filename 3: line */ + $this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %1$s in %2$s on line %3$s', 'action-scheduler' ), $error['message'], $error['file'], $error['line'] ) ); + } + } + + public function log_reset_action( $action_id ) { + $this->log( $action_id, __( 'action reset', 'action-scheduler' ) ); + } + + public function log_ignored_action( $action_id, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action ignored via %s', 'action-scheduler' ), $context ); + } else { + $message = __( 'action ignored', 'action-scheduler' ); + } + $this->log( $action_id, $message ); + } + + /** + * @param string $action_id + * @param Exception|NULL $exception The exception which occured when fetching the action. NULL by default for backward compatibility. + * + * @return ActionScheduler_LogEntry[] + */ + public function log_failed_fetch_action( $action_id, Exception $exception = NULL ) { + + if ( ! is_null( $exception ) ) { + /* translators: %s: exception message */ + $log_message = sprintf( __( 'There was a failure fetching this action: %s', 'action-scheduler' ), $exception->getMessage() ); + } else { + $log_message = __( 'There was a failure fetching this action', 'action-scheduler' ); + } + + $this->log( $action_id, $log_message ); + } + + public function log_failed_schedule_next_instance( $action_id, Exception $exception ) { + /* translators: %s: exception message */ + $this->log( $action_id, sprintf( __( 'There was a failure scheduling the next instance of this action: %s', 'action-scheduler' ), $exception->getMessage() ) ); + } + + /** + * Bulk add cancel action log entries. + * + * Implemented here for backward compatibility. Should be implemented in parent loggers + * for more performant bulk logging. + * + * @param array $action_ids List of action ID. + */ + public function bulk_log_cancel_actions( $action_ids ) { + if ( empty( $action_ids ) ) { + return; + } + + foreach ( $action_ids as $action_id ) { + $this->log_canceled_action( $action_id ); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php new file mode 100644 index 0000000000..6b71a77938 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php @@ -0,0 +1,422 @@ +<?php + +/** + * Class ActionScheduler_Store + * @codeCoverageIgnore + */ +abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated { + const STATUS_COMPLETE = 'complete'; + const STATUS_PENDING = 'pending'; + const STATUS_RUNNING = 'in-progress'; + const STATUS_FAILED = 'failed'; + const STATUS_CANCELED = 'canceled'; + const DEFAULT_CLASS = 'ActionScheduler_wpPostStore'; + + /** @var ActionScheduler_Store */ + private static $store = NULL; + + /** @var int */ + protected static $max_args_length = 191; + + /** + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date Optional Date of the first instance + * to store. Otherwise uses the first date of the action's + * schedule. + * + * @return int The action ID + */ + abstract public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ); + + /** + * @param string $action_id + * + * @return ActionScheduler_Action + */ + abstract public function fetch_action( $action_id ); + + /** + * Find an action. + * + * Note: the query ordering changes based on the passed 'status' value. + * + * @param string $hook Action hook. + * @param array $params Parameters of the action to find. + * + * @return string|null ID of the next action matching the criteria or NULL if not found. + */ + public function find_action( $hook, $params = array() ) { + $params = wp_parse_args( + $params, + array( + 'args' => null, + 'status' => self::STATUS_PENDING, + 'group' => '', + ) + ); + + // These params are fixed for this method. + $params['hook'] = $hook; + $params['orderby'] = 'date'; + $params['per_page'] = 1; + + if ( ! empty( $params['status'] ) ) { + if ( self::STATUS_PENDING === $params['status'] ) { + $params['order'] = 'ASC'; // Find the next action that matches. + } else { + $params['order'] = 'DESC'; // Find the most recent action that matches. + } + } + + $results = $this->query_actions( $params ); + + return empty( $results ) ? null : $results[0]; + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @param array $query { + * Query filtering options. + * + * @type string $hook The name of the actions. Optional. + * @type string|array $status The status or statuses of the actions. Optional. + * @type array $args The args array of the actions. Optional. + * @type DateTime $date The scheduled date of the action. Used in UTC timezone. Optional. + * @type string $date_compare Operator for selecting by $date param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. + * @type DateTime $modified The last modified date of the action. Used in UTC timezone. Optional. + * @type string $modified_compare Operator for comparing $modified param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. + * @type string $group The group the action belongs to. Optional. + * @type bool|int $claimed TRUE to find claimed actions, FALSE to find unclaimed actions, an int to find a specific claim ID. Optional. + * @type int $per_page Number of results to return. Defaults to 5. + * @type int $offset The query pagination offset. Defaults to 0. + * @type int $orderby Accepted values are 'hook', 'group', 'modified', 'date' or 'none'. Defaults to 'date'. + * @type string $order Accepted values are 'ASC' or 'DESC'. Defaults to 'ASC'. + * } + * @param string $query_type Whether to select or count the results. Default, select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + abstract public function query_actions( $query = array(), $query_type = 'select' ); + + /** + * Run query to get a single action ID. + * + * @since x.x.x + * + * @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used. + * + * @param array $query Query parameters. + * + * @return int|null + */ + public function query_action( $query ) { + $query['per_page'] = 1; + $query['offset'] = 0; + $results = $this->query_actions( $query ); + + if ( empty( $results ) ) { + return null; + } else { + return (int) $results[0]; + } + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array + */ + abstract public function action_counts(); + + /** + * @param string $action_id + */ + abstract public function cancel_action( $action_id ); + + /** + * @param string $action_id + */ + abstract public function delete_action( $action_id ); + + /** + * @param string $action_id + * + * @return DateTime The date the action is schedule to run, or the date that it ran. + */ + abstract public function get_date( $action_id ); + + + /** + * @param int $max_actions + * @param DateTime $before_date Claim only actions schedule before the given date. Defaults to now. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return ActionScheduler_ActionClaim + */ + abstract public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ); + + /** + * @return int + */ + abstract public function get_claim_count(); + + /** + * @param ActionScheduler_ActionClaim $claim + */ + abstract public function release_claim( ActionScheduler_ActionClaim $claim ); + + /** + * @param string $action_id + */ + abstract public function unclaim_action( $action_id ); + + /** + * @param string $action_id + */ + abstract public function mark_failure( $action_id ); + + /** + * @param string $action_id + */ + abstract public function log_execution( $action_id ); + + /** + * @param string $action_id + */ + abstract public function mark_complete( $action_id ); + + /** + * @param string $action_id + * + * @return string + */ + abstract public function get_status( $action_id ); + + /** + * @param string $action_id + * @return mixed + */ + abstract public function get_claim_id( $action_id ); + + /** + * @param string $claim_id + * @return array + */ + abstract public function find_actions_by_claim_id( $claim_id ); + + /** + * @param string $comparison_operator + * @return string + */ + protected function validate_sql_comparator( $comparison_operator ) { + if ( in_array( $comparison_operator, array('!=', '>', '>=', '<', '<=', '=') ) ) { + return $comparison_operator; + } + return '='; + } + + /** + * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + protected function get_scheduled_date_string( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { + $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; + if ( ! $next ) { + return '0000-00-00 00:00:00'; + } + $next->setTimezone( new DateTimeZone( 'UTC' ) ); + + return $next->format( 'Y-m-d H:i:s' ); + } + + /** + * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + protected function get_scheduled_date_string_local( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { + $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; + if ( ! $next ) { + return '0000-00-00 00:00:00'; + } + + ActionScheduler_TimezoneHelper::set_local_timezone( $next ); + return $next->format( 'Y-m-d H:i:s' ); + } + + /** + * Validate that we could decode action arguments. + * + * @param mixed $args The decoded arguments. + * @param int $action_id The action ID. + * + * @throws ActionScheduler_InvalidActionException When the decoded arguments are invalid. + */ + protected function validate_args( $args, $action_id ) { + // Ensure we have an array of args. + if ( ! is_array( $args ) ) { + throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id ); + } + + // Validate JSON decoding if possible. + if ( function_exists( 'json_last_error' ) && JSON_ERROR_NONE !== json_last_error() ) { + throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id, $args ); + } + } + + /** + * Validate a ActionScheduler_Schedule object. + * + * @param mixed $schedule The unserialized ActionScheduler_Schedule object. + * @param int $action_id The action ID. + * + * @throws ActionScheduler_InvalidActionException When the schedule is invalid. + */ + protected function validate_schedule( $schedule, $action_id ) { + if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) { + throw ActionScheduler_InvalidActionException::from_schedule( $action_id, $schedule ); + } + } + + /** + * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. + * + * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, + * with custom tables, we use an indexed VARCHAR column instead. + * + * @param ActionScheduler_Action $action Action to be validated. + * @throws InvalidArgumentException When json encoded args is too long. + */ + protected function validate_action( ActionScheduler_Action $action ) { + if ( strlen( json_encode( $action->get_args() ) ) > static::$max_args_length ) { + throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'action-scheduler' ), static::$max_args_length ) ); + } + } + + /** + * Cancel pending actions by hook. + * + * @since 3.0.0 + * + * @param string $hook Hook name. + * + * @return void + */ + public function cancel_actions_by_hook( $hook ) { + $action_ids = true; + while ( ! empty( $action_ids ) ) { + $action_ids = $this->query_actions( + array( + 'hook' => $hook, + 'status' => self::STATUS_PENDING, + 'per_page' => 1000, + 'orderby' => 'action_id', + ) + ); + + $this->bulk_cancel_actions( $action_ids ); + } + } + + /** + * Cancel pending actions by group. + * + * @since 3.0.0 + * + * @param string $group Group slug. + * + * @return void + */ + public function cancel_actions_by_group( $group ) { + $action_ids = true; + while ( ! empty( $action_ids ) ) { + $action_ids = $this->query_actions( + array( + 'group' => $group, + 'status' => self::STATUS_PENDING, + 'per_page' => 1000, + 'orderby' => 'action_id', + ) + ); + + $this->bulk_cancel_actions( $action_ids ); + } + } + + /** + * Cancel a set of action IDs. + * + * @since 3.0.0 + * + * @param array $action_ids List of action IDs. + * + * @return void + */ + private function bulk_cancel_actions( $action_ids ) { + foreach ( $action_ids as $action_id ) { + $this->cancel_action( $action_id ); + } + + do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); + } + + /** + * @return array + */ + public function get_status_labels() { + return array( + self::STATUS_COMPLETE => __( 'Complete', 'action-scheduler' ), + self::STATUS_PENDING => __( 'Pending', 'action-scheduler' ), + self::STATUS_RUNNING => __( 'In-progress', 'action-scheduler' ), + self::STATUS_FAILED => __( 'Failed', 'action-scheduler' ), + self::STATUS_CANCELED => __( 'Canceled', 'action-scheduler' ), + ); + } + + /** + * Check if there are any pending scheduled actions due to run. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + public function has_pending_actions_due() { + $pending_actions = $this->query_actions( array( + 'date' => as_get_datetime_object(), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'orderby' => 'none', + ) ); + + return ! empty( $pending_actions ); + } + + /** + * Callable initialization function optionally overridden in derived classes. + */ + public function init() {} + + /** + * Callable function to mark an action as migrated optionally overridden in derived classes. + */ + public function mark_migrated( $action_id ) {} + + /** + * @return ActionScheduler_Store + */ + public static function instance() { + if ( empty( self::$store ) ) { + $class = apply_filters( 'action_scheduler_store_class', self::DEFAULT_CLASS ); + self::$store = new $class(); + } + return self::$store; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php new file mode 100644 index 0000000000..fd01449412 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php @@ -0,0 +1,152 @@ +<?php + +/** + * Class ActionScheduler_TimezoneHelper + */ +abstract class ActionScheduler_TimezoneHelper { + private static $local_timezone = NULL; + + /** + * Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset + * if no timezone string is available. + * + * @since 2.1.0 + * + * @param DateTime $date + * @return ActionScheduler_DateTime + */ + public static function set_local_timezone( DateTime $date ) { + + // Accept a DateTime for easier backward compatibility, even though we require methods on ActionScheduler_DateTime + if ( ! is_a( $date, 'ActionScheduler_DateTime' ) ) { + $date = as_get_datetime_object( $date->format( 'U' ) ); + } + + if ( get_option( 'timezone_string' ) ) { + $date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) ); + } else { + $date->setUtcOffset( self::get_local_timezone_offset() ); + } + + return $date; + } + + /** + * Helper to retrieve the timezone string for a site until a WP core method exists + * (see https://core.trac.wordpress.org/ticket/24730). + * + * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. + * + * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone + * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's + * timezone. + * + * @since 2.1.0 + * @return string PHP timezone string for the site or empty if no timezone string is available. + */ + protected static function get_local_timezone_string( $reset = false ) { + // If site timezone string exists, return it. + $timezone = get_option( 'timezone_string' ); + if ( $timezone ) { + return $timezone; + } + + // Get UTC offset, if it isn't set then return UTC. + $utc_offset = intval( get_option( 'gmt_offset', 0 ) ); + if ( 0 === $utc_offset ) { + return 'UTC'; + } + + // Adjust UTC offset from hours to seconds. + $utc_offset *= 3600; + + // Attempt to guess the timezone string from the UTC offset. + $timezone = timezone_name_from_abbr( '', $utc_offset ); + if ( $timezone ) { + return $timezone; + } + + // Last try, guess timezone string manually. + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { + return $city['timezone_id']; + } + } + } + + // No timezone string + return ''; + } + + /** + * Get timezone offset in seconds. + * + * @since 2.1.0 + * @return float + */ + protected static function get_local_timezone_offset() { + $timezone = get_option( 'timezone_string' ); + + if ( $timezone ) { + $timezone_object = new DateTimeZone( $timezone ); + return $timezone_object->getOffset( new DateTime( 'now' ) ); + } else { + return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; + } + } + + /** + * @deprecated 2.1.0 + */ + public static function get_local_timezone( $reset = FALSE ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); + if ( $reset ) { + self::$local_timezone = NULL; + } + if ( !isset(self::$local_timezone) ) { + $tzstring = get_option('timezone_string'); + + if ( empty($tzstring) ) { + $gmt_offset = get_option('gmt_offset'); + if ( $gmt_offset == 0 ) { + $tzstring = 'UTC'; + } else { + $gmt_offset *= HOUR_IN_SECONDS; + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 ); + + // If there's no timezone string, try again with no DST. + if ( false === $tzstring ) { + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 ); + } + + // Try mapping to the first abbreviation we can find. + if ( false === $tzstring ) { + $is_dst = date( 'I' ); + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { + // If there's no valid timezone ID, keep looking. + if ( null === $city['timezone_id'] ) { + continue; + } + + $tzstring = $city['timezone_id']; + break 2; + } + } + } + } + + // If we still have no valid string, then fall back to UTC. + if ( false === $tzstring ) { + $tzstring = 'UTC'; + } + } + } + + self::$local_timezone = new DateTimeZone($tzstring); + } + return self::$local_timezone; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php new file mode 100644 index 0000000000..520f932af3 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php @@ -0,0 +1,75 @@ +<?php + +/** + * Class ActionScheduler_Action + */ +class ActionScheduler_Action { + protected $hook = ''; + protected $args = array(); + /** @var ActionScheduler_Schedule */ + protected $schedule = NULL; + protected $group = ''; + + public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) { + $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; + $this->set_hook($hook); + $this->set_schedule($schedule); + $this->set_args($args); + $this->set_group($group); + } + + public function execute() { + return do_action_ref_array( $this->get_hook(), array_values( $this->get_args() ) ); + } + + /** + * @param string $hook + */ + protected function set_hook( $hook ) { + $this->hook = $hook; + } + + public function get_hook() { + return $this->hook; + } + + protected function set_schedule( ActionScheduler_Schedule $schedule ) { + $this->schedule = $schedule; + } + + /** + * @return ActionScheduler_Schedule + */ + public function get_schedule() { + return $this->schedule; + } + + protected function set_args( array $args ) { + $this->args = $args; + } + + public function get_args() { + return $this->args; + } + + /** + * @param string $group + */ + protected function set_group( $group ) { + $this->group = $group; + } + + /** + * @return string + */ + public function get_group() { + return $this->group; + } + + /** + * @return bool If the action has been finished + */ + public function is_finished() { + return FALSE; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php new file mode 100644 index 0000000000..8bbc5d18d5 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php @@ -0,0 +1,23 @@ +<?php + +/** + * Class ActionScheduler_CanceledAction + * + * Stored action which was canceled and therefore acts like a finished action but should always return a null schedule, + * regardless of schedule passed to its constructor. + */ +class ActionScheduler_CanceledAction extends ActionScheduler_FinishedAction { + + /** + * @param string $hook + * @param array $args + * @param ActionScheduler_Schedule $schedule + * @param string $group + */ + public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { + parent::__construct( $hook, $args, $schedule, $group ); + if ( is_null( $schedule ) ) { + $this->set_schedule( new ActionScheduler_NullSchedule() ); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php new file mode 100644 index 0000000000..b23a56c66b --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php @@ -0,0 +1,16 @@ +<?php + +/** + * Class ActionScheduler_FinishedAction + */ +class ActionScheduler_FinishedAction extends ActionScheduler_Action { + + public function execute() { + // don't execute + } + + public function is_finished() { + return TRUE; + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_NullAction.php b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_NullAction.php new file mode 100644 index 0000000000..cd5dc3b0f9 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/actions/ActionScheduler_NullAction.php @@ -0,0 +1,16 @@ +<?php + +/** + * Class ActionScheduler_NullAction + */ +class ActionScheduler_NullAction extends ActionScheduler_Action { + + public function __construct( $hook = '', array $args = array(), ActionScheduler_Schedule $schedule = NULL ) { + $this->set_schedule( new ActionScheduler_NullSchedule() ); + } + + public function execute() { + // don't execute + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php new file mode 100644 index 0000000000..37bfd0d44e --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php @@ -0,0 +1,154 @@ +<?php + +/** + * Class ActionScheduler_DBLogger + * + * Action logs data table data store. + * + * @since 3.0.0 + */ +class ActionScheduler_DBLogger extends ActionScheduler_Logger { + + /** + * Add a record to an action log. + * + * @param int $action_id Action ID. + * @param string $message Message to be saved in the log entry. + * @param DateTime $date Timestamp of the log entry. + * + * @return int The log entry ID. + */ + public function log( $action_id, $message, DateTime $date = null ) { + if ( empty( $date ) ) { + $date = as_get_datetime_object(); + } else { + $date = clone $date; + } + + $date_gmt = $date->format( 'Y-m-d H:i:s' ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $date_local = $date->format( 'Y-m-d H:i:s' ); + + /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort + global $wpdb; + $wpdb->insert( + $wpdb->actionscheduler_logs, + array( + 'action_id' => $action_id, + 'message' => $message, + 'log_date_gmt' => $date_gmt, + 'log_date_local' => $date_local, + ), + array( '%d', '%s', '%s', '%s' ) + ); + + return $wpdb->insert_id; + } + + /** + * Retrieve an action log entry. + * + * @param int $entry_id Log entry ID. + * + * @return ActionScheduler_LogEntry + */ + public function get_entry( $entry_id ) { + /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort + global $wpdb; + $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) ); + + return $this->create_entry_from_db_record( $entry ); + } + + /** + * Create an action log entry from a database record. + * + * @param object $record Log entry database record object. + * + * @return ActionScheduler_LogEntry + */ + private function create_entry_from_db_record( $record ) { + if ( empty( $record ) ) { + return new ActionScheduler_NullLogEntry(); + } + + if ( is_null( $record->log_date_gmt ) ) { + $date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE ); + } else { + $date = as_get_datetime_object( $record->log_date_gmt ); + } + + return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date ); + } + + /** + * Retrieve the an action's log entries from the database. + * + * @param int $action_id Action ID. + * + * @return ActionScheduler_LogEntry[] + */ + public function get_logs( $action_id ) { + /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort + global $wpdb; + + $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) ); + + return array_map( array( $this, 'create_entry_from_db_record' ), $records ); + } + + /** + * Initialize the data store. + * + * @codeCoverageIgnore + */ + public function init() { + $table_maker = new ActionScheduler_LoggerSchema(); + $table_maker->init(); + $table_maker->register_tables(); + + parent::init(); + + add_action( 'action_scheduler_deleted_action', array( $this, 'clear_deleted_action_logs' ), 10, 1 ); + } + + /** + * Delete the action logs for an action. + * + * @param int $action_id Action ID. + */ + public function clear_deleted_action_logs( $action_id ) { + /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort + global $wpdb; + $wpdb->delete( $wpdb->actionscheduler_logs, array( 'action_id' => $action_id ), array( '%d' ) ); + } + + /** + * Bulk add cancel action log entries. + * + * @param array $action_ids List of action ID. + */ + public function bulk_log_cancel_actions( $action_ids ) { + if ( empty( $action_ids ) ) { + return; + } + + /** @var \wpdb $wpdb */ //phpcs:ignore Generic.Commenting.DocComment.MissingShort + global $wpdb; + $date = as_get_datetime_object(); + $date_gmt = $date->format( 'Y-m-d H:i:s' ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $date_local = $date->format( 'Y-m-d H:i:s' ); + $message = __( 'action canceled', 'action-scheduler' ); + $format = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')'; + $sql_query = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES "; + $value_rows = array(); + + foreach ( $action_ids as $action_id ) { + $value_rows[] = $wpdb->prepare( $format, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + $sql_query .= implode( ',', $value_rows ); + + $wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php new file mode 100644 index 0000000000..f083764199 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php @@ -0,0 +1,868 @@ +<?php + +/** + * Class ActionScheduler_DBStore + * + * Action data table data store. + * + * @since 3.0.0 + */ +class ActionScheduler_DBStore extends ActionScheduler_Store { + + /** + * Used to share information about the before_date property of claims internally. + * + * This is used in preference to passing the same information as a method param + * for backwards-compatibility reasons. + * + * @var DateTime|null + */ + private $claim_before_date = null; + + /** @var int */ + protected static $max_args_length = 8000; + + /** @var int */ + protected static $max_index_length = 191; + + /** + * Initialize the data store + * + * @codeCoverageIgnore + */ + public function init() { + $table_maker = new ActionScheduler_StoreSchema(); + $table_maker->init(); + $table_maker->register_tables(); + } + + /** + * Save an action. + * + * @param ActionScheduler_Action $action Action object. + * @param DateTime $date Optional schedule date. Default null. + * + * @return int Action ID. + * @throws RuntimeException Throws exception when saving the action fails. + */ + public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) { + try { + + $this->validate_action( $action ); + + /** @var \wpdb $wpdb */ + global $wpdb; + $data = array( + 'hook' => $action->get_hook(), + 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ), + 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), + 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), + 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + 'group_id' => $this->get_group_id( $action->get_group() ), + ); + $args = wp_json_encode( $action->get_args() ); + if ( strlen( $args ) <= static::$max_index_length ) { + $data['args'] = $args; + } else { + $data['args'] = $this->hash_args( $args ); + $data['extended_args'] = $args; + } + + $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions'; + $wpdb->insert( $table_name, $data ); + $action_id = $wpdb->insert_id; + + if ( is_wp_error( $action_id ) ) { + throw new \RuntimeException( $action_id->get_error_message() ); + } elseif ( empty( $action_id ) ) { + throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) ); + } + + do_action( 'action_scheduler_stored_action', $action_id ); + + return $action_id; + } catch ( \Exception $e ) { + /* translators: %s: error message */ + throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); + } + } + + /** + * Generate a hash from json_encoded $args using MD5 as this isn't for security. + * + * @param string $args JSON encoded action args. + * @return string + */ + protected function hash_args( $args ) { + return md5( $args ); + } + + /** + * Get action args query param value from action args. + * + * @param array $args Action args. + * @return string + */ + protected function get_args_for_query( $args ) { + $encoded = wp_json_encode( $args ); + if ( strlen( $encoded ) <= static::$max_index_length ) { + return $encoded; + } + return $this->hash_args( $encoded ); + } + /** + * Get a group's ID based on its name/slug. + * + * @param string $slug The string name of a group. + * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. + * + * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. + */ + protected function get_group_id( $slug, $create_if_not_exists = true ) { + if ( empty( $slug ) ) { + return 0; + } + /** @var \wpdb $wpdb */ + global $wpdb; + $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); + if ( empty( $group_id ) && $create_if_not_exists ) { + $group_id = $this->create_group( $slug ); + } + + return $group_id; + } + + /** + * Create an action group. + * + * @param string $slug Group slug. + * + * @return int Group ID. + */ + protected function create_group( $slug ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->insert( $wpdb->actionscheduler_groups, array( 'slug' => $slug ) ); + + return (int) $wpdb->insert_id; + } + + /** + * Retrieve an action. + * + * @param int $action_id Action ID. + * + * @return ActionScheduler_Action + */ + public function fetch_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $data = $wpdb->get_row( + $wpdb->prepare( + "SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d", + $action_id + ) + ); + + if ( empty( $data ) ) { + return $this->get_null_action(); + } + + if ( ! empty( $data->extended_args ) ) { + $data->args = $data->extended_args; + unset( $data->extended_args ); + } + + // Convert NULL dates to zero dates. + $date_fields = array( + 'scheduled_date_gmt', + 'scheduled_date_local', + 'last_attempt_gmt', + 'last_attempt_gmt', + ); + foreach ( $date_fields as $date_field ) { + if ( is_null( $data->$date_field ) ) { + $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE; + } + } + + try { + $action = $this->make_action_from_db_record( $data ); + } catch ( ActionScheduler_InvalidActionException $exception ) { + do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception ); + return $this->get_null_action(); + } + + return $action; + } + + /** + * Create a null action. + * + * @return ActionScheduler_NullAction + */ + protected function get_null_action() { + return new ActionScheduler_NullAction(); + } + + /** + * Create an action from a database record. + * + * @param object $data Action database record. + * + * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction + */ + protected function make_action_from_db_record( $data ) { + + $hook = $data->hook; + $args = json_decode( $data->args, true ); + $schedule = unserialize( $data->schedule ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize + + $this->validate_args( $args, $data->action_id ); + $this->validate_schedule( $schedule, $data->action_id ); + + if ( empty( $schedule ) ) { + $schedule = new ActionScheduler_NullSchedule(); + } + $group = $data->group ? $data->group : ''; + + return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group ); + } + + /** + * Returns the SQL statement to query (or count) actions. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @param array $query Filtering options. + * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count. + * + * @return string SQL statement already properly escaped. + * @throws InvalidArgumentException If the query is invalid. + */ + protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { + + if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) { + throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) ); + } + + $query = wp_parse_args( + $query, + array( + 'hook' => '', + 'args' => null, + 'date' => null, + 'date_compare' => '<=', + 'modified' => null, + 'modified_compare' => '<=', + 'group' => '', + 'status' => '', + 'claimed' => null, + 'per_page' => 5, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + ) + ); + + /** @var \wpdb $wpdb */ + global $wpdb; + $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id'; + $sql .= " FROM {$wpdb->actionscheduler_actions} a"; + $sql_params = array(); + + if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) { + $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id"; + } + + $sql .= ' WHERE 1=1'; + + if ( ! empty( $query['group'] ) ) { + $sql .= ' AND g.slug=%s'; + $sql_params[] = $query['group']; + } + + if ( $query['hook'] ) { + $sql .= ' AND a.hook=%s'; + $sql_params[] = $query['hook']; + } + if ( ! is_null( $query['args'] ) ) { + $sql .= ' AND a.args=%s'; + $sql_params[] = $this->get_args_for_query( $query['args'] ); + } + + if ( $query['status'] ) { + $statuses = (array) $query['status']; + $placeholders = array_fill( 0, count( $statuses ), '%s' ); + $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')'; + $sql_params = array_merge( $sql_params, array_values( $statuses ) ); + } + + if ( $query['date'] instanceof \DateTime ) { + $date = clone $query['date']; + $date->setTimezone( new \DateTimeZone( 'UTC' ) ); + $date_string = $date->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query['date_compare'] ); + $sql .= " AND a.scheduled_date_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query['modified'] instanceof \DateTime ) { + $modified = clone $query['modified']; + $modified->setTimezone( new \DateTimeZone( 'UTC' ) ); + $date_string = $modified->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query['modified_compare'] ); + $sql .= " AND a.last_attempt_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( true === $query['claimed'] ) { + $sql .= ' AND a.claim_id != 0'; + } elseif ( false === $query['claimed'] ) { + $sql .= ' AND a.claim_id = 0'; + } elseif ( ! is_null( $query['claimed'] ) ) { + $sql .= ' AND a.claim_id = %d'; + $sql_params[] = $query['claimed']; + } + + if ( ! empty( $query['search'] ) ) { + $sql .= ' AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s'; + for ( $i = 0; $i < 3; $i++ ) { + $sql_params[] = sprintf( '%%%s%%', $query['search'] ); + } + + $search_claim_id = (int) $query['search']; + if ( $search_claim_id ) { + $sql .= ' OR a.claim_id = %d'; + $sql_params[] = $search_claim_id; + } + + $sql .= ')'; + } + + if ( 'select' === $select_or_count ) { + if ( 'ASC' === strtoupper( $query['order'] ) ) { + $order = 'ASC'; + } else { + $order = 'DESC'; + } + switch ( $query['orderby'] ) { + case 'hook': + $sql .= " ORDER BY a.hook $order"; + break; + case 'group': + $sql .= " ORDER BY g.slug $order"; + break; + case 'modified': + $sql .= " ORDER BY a.last_attempt_gmt $order"; + break; + case 'none': + break; + case 'action_id': + $sql .= " ORDER BY a.action_id $order"; + break; + case 'date': + default: + $sql .= " ORDER BY a.scheduled_date_gmt $order"; + break; + } + + if ( $query['per_page'] > 0 ) { + $sql .= ' LIMIT %d, %d'; + $sql_params[] = $query['offset']; + $sql_params[] = $query['per_page']; + } + } + + if ( ! empty( $sql_params ) ) { + $sql = $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + return $sql; + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @see ActionScheduler_Store::query_actions for $query arg usage. + * + * @param array $query Query filtering options. + * @param string $query_type Whether to select or count the results. Defaults to select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + public function query_actions( $query = array(), $query_type = 'select' ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $sql = $this->get_query_actions_sql( $query, $query_type ); + + return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoSql, WordPress.DB.DirectDatabaseQuery.NoCaching + } + + /** + * Get a count of all actions in the store, grouped by status. + * + * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. + */ + public function action_counts() { + global $wpdb; + + $sql = "SELECT a.status, count(a.status) as 'count'"; + $sql .= " FROM {$wpdb->actionscheduler_actions} a"; + $sql .= ' GROUP BY a.status'; + + $actions_count_by_status = array(); + $action_stati_and_labels = $this->get_status_labels(); + + foreach ( $wpdb->get_results( $sql ) as $action_data ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + // Ignore any actions with invalid status. + if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) { + $actions_count_by_status[ $action_data->status ] = $action_data->count; + } + } + + return $actions_count_by_status; + } + + /** + * Cancel an action. + * + * @param int $action_id Action ID. + * + * @return void + * @throws \InvalidArgumentException If the action update failed. + */ + public function cancel_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + array( 'status' => self::STATUS_CANCELED ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + if ( empty( $updated ) ) { + /* translators: %s: action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + } + do_action( 'action_scheduler_canceled_action', $action_id ); + } + + /** + * Cancel pending actions by hook. + * + * @since 3.0.0 + * + * @param string $hook Hook name. + * + * @return void + */ + public function cancel_actions_by_hook( $hook ) { + $this->bulk_cancel_actions( array( 'hook' => $hook ) ); + } + + /** + * Cancel pending actions by group. + * + * @param string $group Group slug. + * + * @return void + */ + public function cancel_actions_by_group( $group ) { + $this->bulk_cancel_actions( array( 'group' => $group ) ); + } + + /** + * Bulk cancel actions. + * + * @since 3.0.0 + * + * @param array $query_args Query parameters. + */ + protected function bulk_cancel_actions( $query_args ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + if ( ! is_array( $query_args ) ) { + return; + } + + // Don't cancel actions that are already canceled. + if ( isset( $query_args['status'] ) && self::STATUS_CANCELED === $query_args['status'] ) { + return; + } + + $action_ids = true; + $query_args = wp_parse_args( + $query_args, + array( + 'per_page' => 1000, + 'status' => self::STATUS_PENDING, + 'orderby' => 'action_id', + ) + ); + + while ( $action_ids ) { + $action_ids = $this->query_actions( $query_args ); + if ( empty( $action_ids ) ) { + break; + } + + $format = array_fill( 0, count( $action_ids ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $parameters = $action_ids; + array_unshift( $parameters, self::STATUS_CANCELED ); + + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $parameters + ) + ); + + do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); + } + } + + /** + * Delete an action. + * + * @param int $action_id Action ID. + * @throws \InvalidArgumentException If the action deletion failed. + */ + public function delete_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, array( 'action_id' => $action_id ), array( '%d' ) ); + if ( empty( $deleted ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + } + do_action( 'action_scheduler_deleted_action', $action_id ); + } + + /** + * Get the schedule date for an action. + * + * @param string $action_id Action ID. + * + * @return \DateTime The local date the action is scheduled to run, or the date that it ran. + */ + public function get_date( $action_id ) { + $date = $this->get_date_gmt( $action_id ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + return $date; + } + + /** + * Get the GMT schedule date for an action. + * + * @param int $action_id Action ID. + * + * @throws \InvalidArgumentException If action cannot be identified. + * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran. + */ + protected function get_date_gmt( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) ); + if ( empty( $record ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + } + if ( self::STATUS_PENDING === $record->status ) { + return as_get_datetime_object( $record->scheduled_date_gmt ); + } else { + return as_get_datetime_object( $record->last_attempt_gmt ); + } + } + + /** + * Stake a claim on actions. + * + * @param int $max_actions Maximum number of action to include in claim. + * @param \DateTime $before_date Jobs must be schedule before this date. Defaults to now. + * @param array $hooks Hooks to filter for. + * @param string $group Group to filter for. + * + * @return ActionScheduler_ActionClaim + */ + public function stake_claim( $max_actions = 10, \DateTime $before_date = null, $hooks = array(), $group = '' ) { + $claim_id = $this->generate_claim_id(); + + $this->claim_before_date = $before_date; + $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); + $action_ids = $this->find_actions_by_claim_id( $claim_id ); + $this->claim_before_date = null; + + return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); + } + + /** + * Generate a new action claim. + * + * @return int Claim ID. + */ + protected function generate_claim_id() { + /** @var \wpdb $wpdb */ + global $wpdb; + $now = as_get_datetime_object(); + $wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) ); + + return $wpdb->insert_id; + } + + /** + * Mark actions claimed. + * + * @param string $claim_id Claim Id. + * @param int $limit Number of action to include in claim. + * @param \DateTime $before_date Should use UTC timezone. + * @param array $hooks Hooks to filter for. + * @param string $group Group to filter for. + * + * @return int The number of actions that were claimed. + * @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist. + * @throws \RuntimeException Throws RuntimeException if unable to claim action. + */ + protected function claim_actions( $claim_id, $limit, \DateTime $before_date = null, $hooks = array(), $group = '' ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $now = as_get_datetime_object(); + $date = is_null( $before_date ) ? $now : clone $before_date; + + // can't use $wpdb->update() because of the <= condition. + $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; + $params = array( + $claim_id, + $now->format( 'Y-m-d H:i:s' ), + current_time( 'mysql' ), + ); + + $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s'; + $params[] = $date->format( 'Y-m-d H:i:s' ); + $params[] = self::STATUS_PENDING; + + if ( ! empty( $hooks ) ) { + $placeholders = array_fill( 0, count( $hooks ), '%s' ); + $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; + $params = array_merge( $params, array_values( $hooks ) ); + } + + if ( ! empty( $group ) ) { + + $group_id = $this->get_group_id( $group, false ); + + // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour. + if ( empty( $group_id ) ) { + /* translators: %s: group name */ + throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); + } + + $where .= ' AND group_id = %d'; + $params[] = $group_id; + } + + /** + * Sets the order-by clause used in the action claim query. + * + * @since x.x.x + * + * @param string $order_by_sql + */ + $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); + $params[] = $limit; + + $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders + $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + if ( false === $rows_affected ) { + throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); + } + + return (int) $rows_affected; + } + + /** + * Get the number of active claims. + * + * @return int + */ + public function get_claim_count() { + global $wpdb; + + $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)"; + $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Return an action's claim ID, as stored in the claim_id column. + * + * @param string $action_id Action ID. + * @return mixed + */ + public function get_claim_id( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; + $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Retrieve the action IDs of action in a claim. + * + * @param int $claim_id Claim ID. + * @return int[] + */ + public function find_actions_by_claim_id( $claim_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $action_ids = array(); + $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); + $cut_off = $before_date->format( 'Y-m-d H:i:s' ); + + $sql = $wpdb->prepare( + "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", + $claim_id + ); + + // Verify that the scheduled date for each action is within the expected bounds (in some unusual + // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). + foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + if ( $claimed_action->scheduled_date_gmt <= $cut_off ) { + $action_ids[] = absint( $claimed_action->action_id ); + } + } + + return $action_ids; + } + + /** + * Release actions from a claim and delete the claim. + * + * @param ActionScheduler_ActionClaim $claim Claim object. + */ + public function release_claim( ActionScheduler_ActionClaim $claim ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'claim_id' => $claim->get_id() ), array( '%d' ), array( '%d' ) ); + $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) ); + } + + /** + * Remove the claim from an action. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function unclaim_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->update( + $wpdb->actionscheduler_actions, + array( 'claim_id' => 0 ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + } + + /** + * Mark an action as failed. + * + * @param int $action_id Action ID. + * @throws \InvalidArgumentException Throw an exception if action was not updated. + */ + public function mark_failure( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + array( 'status' => self::STATUS_FAILED ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + if ( empty( $updated ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + } + } + + /** + * Add execution message to action log. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function log_execution( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; + $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Mark an action as complete. + * + * @param int $action_id Action ID. + * + * @return void + * @throws \InvalidArgumentException Throw an exception if action was not updated. + */ + public function mark_complete( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + array( + 'status' => self::STATUS_COMPLETE, + 'last_attempt_gmt' => current_time( 'mysql', true ), + 'last_attempt_local' => current_time( 'mysql' ), + ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + if ( empty( $updated ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment + } + } + + /** + * Get an action's status. + * + * @param int $action_id Action ID. + * + * @return string + * @throws \InvalidArgumentException Throw an exception if not status was found for action_id. + * @throws \RuntimeException Throw an exception if action status could not be retrieved. + */ + public function get_status( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; + $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( null === $status ) { + throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); + } elseif ( empty( $status ) ) { + throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) ); + } else { + return $status; + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php new file mode 100644 index 0000000000..22d61a606a --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php @@ -0,0 +1,426 @@ +<?php + +use ActionScheduler_Store as Store; +use Action_Scheduler\Migration\Runner; +use Action_Scheduler\Migration\Config; +use Action_Scheduler\Migration\Controller; + +/** + * Class ActionScheduler_HybridStore + * + * A wrapper around multiple stores that fetches data from both. + * + * @since 3.0.0 + */ +class ActionScheduler_HybridStore extends Store { + const DEMARKATION_OPTION = 'action_scheduler_hybrid_store_demarkation'; + + private $primary_store; + private $secondary_store; + private $migration_runner; + + /** + * @var int The dividing line between IDs of actions created + * by the primary and secondary stores. + * + * Methods that accept an action ID will compare the ID against + * this to determine which store will contain that ID. In almost + * all cases, the ID should come from the primary store, but if + * client code is bypassing the API functions and fetching IDs + * from elsewhere, then there is a chance that an unmigrated ID + * might be requested. + */ + private $demarkation_id = 0; + + /** + * ActionScheduler_HybridStore constructor. + * + * @param Config $config Migration config object. + */ + public function __construct( Config $config = null ) { + $this->demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 ); + if ( empty( $config ) ) { + $config = Controller::instance()->get_migration_config_object(); + } + $this->primary_store = $config->get_destination_store(); + $this->secondary_store = $config->get_source_store(); + $this->migration_runner = new Runner( $config ); + } + + /** + * Initialize the table data store tables. + * + * @codeCoverageIgnore + */ + public function init() { + add_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10, 2 ); + $this->primary_store->init(); + $this->secondary_store->init(); + remove_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10 ); + } + + /** + * When the actions table is created, set its autoincrement + * value to be one higher than the posts table to ensure that + * there are no ID collisions. + * + * @param string $table_name + * @param string $table_suffix + * + * @return void + * @codeCoverageIgnore + */ + public function set_autoincrement( $table_name, $table_suffix ) { + if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) { + if ( empty( $this->demarkation_id ) ) { + $this->demarkation_id = $this->set_demarkation_id(); + } + /** @var \wpdb $wpdb */ + global $wpdb; + /** + * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with + * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE. + */ + $default_date = new DateTime( 'tomorrow' ); + $null_action = new ActionScheduler_NullAction(); + $date_gmt = $this->get_scheduled_date_string( $null_action, $default_date ); + $date_local = $this->get_scheduled_date_string_local( $null_action, $default_date ); + + $row_count = $wpdb->insert( + $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, + [ + 'action_id' => $this->demarkation_id, + 'hook' => '', + 'status' => '', + 'scheduled_date_gmt' => $date_gmt, + 'scheduled_date_local' => $date_local, + 'last_attempt_gmt' => $date_gmt, + 'last_attempt_local' => $date_local, + ] + ); + if ( $row_count > 0 ) { + $wpdb->delete( + $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, + [ 'action_id' => $this->demarkation_id ] + ); + } + } + } + + /** + * Store the demarkation id in WP options. + * + * @param int $id The ID to set as the demarkation point between the two stores + * Leave null to use the next ID from the WP posts table. + * + * @return int The new ID. + * + * @codeCoverageIgnore + */ + private function set_demarkation_id( $id = null ) { + if ( empty( $id ) ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" ); + $id ++; + } + update_option( self::DEMARKATION_OPTION, $id ); + + return $id; + } + + /** + * Find the first matching action from the secondary store. + * If it exists, migrate it to the primary store immediately. + * After it migrates, the secondary store will logically contain + * the next matching action, so return the result thence. + * + * @param string $hook + * @param array $params + * + * @return string + */ + public function find_action( $hook, $params = [] ) { + $found_unmigrated_action = $this->secondary_store->find_action( $hook, $params ); + if ( ! empty( $found_unmigrated_action ) ) { + $this->migrate( [ $found_unmigrated_action ] ); + } + + return $this->primary_store->find_action( $hook, $params ); + } + + /** + * Find actions matching the query in the secondary source first. + * If any are found, migrate them immediately. Then the secondary + * store will contain the canonical results. + * + * @param array $query + * @param string $query_type Whether to select or count the results. Default, select. + * + * @return int[] + */ + public function query_actions( $query = [], $query_type = 'select' ) { + $found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' ); + if ( ! empty( $found_unmigrated_actions ) ) { + $this->migrate( $found_unmigrated_actions ); + } + + return $this->primary_store->query_actions( $query, $query_type ); + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. + */ + public function action_counts() { + $unmigrated_actions_count = $this->secondary_store->action_counts(); + $migrated_actions_count = $this->primary_store->action_counts(); + $actions_count_by_status = array(); + + foreach ( $this->get_status_labels() as $status_key => $status_label ) { + + $count = 0; + + if ( isset( $unmigrated_actions_count[ $status_key ] ) ) { + $count += $unmigrated_actions_count[ $status_key ]; + } + + if ( isset( $migrated_actions_count[ $status_key ] ) ) { + $count += $migrated_actions_count[ $status_key ]; + } + + $actions_count_by_status[ $status_key ] = $count; + } + + $actions_count_by_status = array_filter( $actions_count_by_status ); + + return $actions_count_by_status; + } + + /** + * If any actions would have been claimed by the secondary store, + * migrate them immediately, then ask the primary store for the + * canonical claim. + * + * @param int $max_actions + * @param DateTime|null $before_date + * + * @return ActionScheduler_ActionClaim + */ + public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) { + $claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); + + $claimed_actions = $claim->get_actions(); + if ( ! empty( $claimed_actions ) ) { + $this->migrate( $claimed_actions ); + } + + $this->secondary_store->release_claim( $claim ); + + return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); + } + + /** + * Migrate a list of actions to the table data store. + * + * @param array $action_ids List of action IDs. + */ + private function migrate( $action_ids ) { + $this->migration_runner->migrate_actions( $action_ids ); + } + + /** + * Save an action to the primary store. + * + * @param ActionScheduler_Action $action Action object to be saved. + * @param DateTime $date Optional. Schedule date. Default null. + * + * @return int The action ID + */ + public function save_action( ActionScheduler_Action $action, DateTime $date = null ) { + return $this->primary_store->save_action( $action, $date ); + } + + /** + * Retrieve an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function fetch_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id, true ); + if ( $store ) { + return $store->fetch_action( $action_id ); + } else { + return new ActionScheduler_NullAction(); + } + } + + /** + * Cancel an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function cancel_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->cancel_action( $action_id ); + } + } + + /** + * Delete an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function delete_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->delete_action( $action_id ); + } + } + + /** + * Get the schedule date an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function get_date( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + return $store->get_date( $action_id ); + } else { + return null; + } + } + + /** + * Mark an existing action as failed whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function mark_failure( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->mark_failure( $action_id ); + } + } + + /** + * Log the execution of an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function log_execution( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->log_execution( $action_id ); + } + } + + /** + * Mark an existing action complete whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function mark_complete( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->mark_complete( $action_id ); + } + } + + /** + * Get an existing action status whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function get_status( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + return $store->get_status( $action_id ); + } + return null; + } + + /** + * Return which store an action is stored in. + * + * @param int $action_id ID of the action. + * @param bool $primary_first Optional flag indicating search the primary store first. + * @return ActionScheduler_Store + */ + protected function get_store_from_action_id( $action_id, $primary_first = false ) { + if ( $primary_first ) { + $stores = [ + $this->primary_store, + $this->secondary_store, + ]; + } elseif ( $action_id < $this->demarkation_id ) { + $stores = [ + $this->secondary_store, + $this->primary_store, + ]; + } else { + $stores = [ + $this->primary_store, + ]; + } + + foreach ( $stores as $store ) { + $action = $store->fetch_action( $action_id ); + if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) { + return $store; + } + } + return null; + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * + * All claim-related functions should operate solely + * on the primary store. + * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * Get the claim count from the table data store. + */ + public function get_claim_count() { + return $this->primary_store->get_claim_count(); + } + + /** + * Retrieve the claim ID for an action from the table data store. + * + * @param int $action_id Action ID. + */ + public function get_claim_id( $action_id ) { + return $this->primary_store->get_claim_id( $action_id ); + } + + /** + * Release a claim in the table data store. + * + * @param ActionScheduler_ActionClaim $claim Claim object. + */ + public function release_claim( ActionScheduler_ActionClaim $claim ) { + $this->primary_store->release_claim( $claim ); + } + + /** + * Release claims on an action in the table data store. + * + * @param int $action_id Action ID. + */ + public function unclaim_action( $action_id ) { + $this->primary_store->unclaim_action( $action_id ); + } + + /** + * Retrieve a list of action IDs by claim. + * + * @param int $claim_id Claim ID. + */ + public function find_actions_by_claim_id( $claim_id ) { + return $this->primary_store->find_actions_by_claim_id( $claim_id ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php new file mode 100644 index 0000000000..7215ddd94a --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php @@ -0,0 +1,240 @@ +<?php + +/** + * Class ActionScheduler_wpCommentLogger + */ +class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { + const AGENT = 'ActionScheduler'; + const TYPE = 'action_log'; + + /** + * @param string $action_id + * @param string $message + * @param DateTime $date + * + * @return string The log entry ID + */ + public function log( $action_id, $message, DateTime $date = NULL ) { + if ( empty($date) ) { + $date = as_get_datetime_object(); + } else { + $date = as_get_datetime_object( clone $date ); + } + $comment_id = $this->create_wp_comment( $action_id, $message, $date ); + return $comment_id; + } + + protected function create_wp_comment( $action_id, $message, DateTime $date ) { + + $comment_date_gmt = $date->format('Y-m-d H:i:s'); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $comment_data = array( + 'comment_post_ID' => $action_id, + 'comment_date' => $date->format('Y-m-d H:i:s'), + 'comment_date_gmt' => $comment_date_gmt, + 'comment_author' => self::AGENT, + 'comment_content' => $message, + 'comment_agent' => self::AGENT, + 'comment_type' => self::TYPE, + ); + return wp_insert_comment($comment_data); + } + + /** + * @param string $entry_id + * + * @return ActionScheduler_LogEntry + */ + public function get_entry( $entry_id ) { + $comment = $this->get_comment( $entry_id ); + if ( empty($comment) || $comment->comment_type != self::TYPE ) { + return new ActionScheduler_NullLogEntry(); + } + + $date = as_get_datetime_object( $comment->comment_date_gmt ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date ); + } + + /** + * @param string $action_id + * + * @return ActionScheduler_LogEntry[] + */ + public function get_logs( $action_id ) { + $status = 'all'; + if ( get_post_status($action_id) == 'trash' ) { + $status = 'post-trashed'; + } + $comments = get_comments(array( + 'post_id' => $action_id, + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'type' => self::TYPE, + 'status' => $status, + )); + $logs = array(); + foreach ( $comments as $c ) { + $entry = $this->get_entry( $c ); + if ( !empty($entry) ) { + $logs[] = $entry; + } + } + return $logs; + } + + protected function get_comment( $comment_id ) { + return get_comment( $comment_id ); + } + + + + /** + * @param WP_Comment_Query $query + */ + public function filter_comment_queries( $query ) { + foreach ( array('ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID') as $key ) { + if ( !empty($query->query_vars[$key]) ) { + return; // don't slow down queries that wouldn't include action_log comments anyway + } + } + $query->query_vars['action_log_filter'] = TRUE; + add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 ); + } + + /** + * @param array $clauses + * @param WP_Comment_Query $query + * + * @return array + */ + public function filter_comment_query_clauses( $clauses, $query ) { + if ( !empty($query->query_vars['action_log_filter']) ) { + $clauses['where'] .= $this->get_where_clause(); + } + return $clauses; + } + + /** + * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not + * the WP_Comment_Query class handled by @see self::filter_comment_queries(). + * + * @param string $where + * @param WP_Query $query + * + * @return string + */ + public function filter_comment_feed( $where, $query ) { + if ( is_comment_feed() ) { + $where .= $this->get_where_clause(); + } + return $where; + } + + /** + * Return a SQL clause to exclude Action Scheduler comments. + * + * @return string + */ + protected function get_where_clause() { + global $wpdb; + return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE ); + } + + /** + * Remove action log entries from wp_count_comments() + * + * @param array $stats + * @param int $post_id + * + * @return object + */ + public function filter_comment_count( $stats, $post_id ) { + global $wpdb; + + if ( 0 === $post_id ) { + $stats = $this->get_comment_count(); + } + + return $stats; + } + + /** + * Retrieve the comment counts from our cache, or the database if the cached version isn't set. + * + * @return object + */ + protected function get_comment_count() { + global $wpdb; + + $stats = get_transient( 'as_comment_count' ); + + if ( ! $stats ) { + $stats = array(); + + $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A ); + + $total = 0; + $stats = array(); + $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed' ); + + foreach ( (array) $count as $row ) { + // Don't count post-trashed toward totals + if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] ) { + $total += $row['num_comments']; + } + if ( isset( $approved[ $row['comment_approved'] ] ) ) { + $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; + } + } + + $stats['total_comments'] = $total; + $stats['all'] = $total; + + foreach ( $approved as $key ) { + if ( empty( $stats[ $key ] ) ) { + $stats[ $key ] = 0; + } + } + + $stats = (object) $stats; + set_transient( 'as_comment_count', $stats ); + } + + return $stats; + } + + /** + * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache + * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called. + */ + public function delete_comment_count_cache() { + delete_transient( 'as_comment_count' ); + } + + /** + * @codeCoverageIgnore + */ + public function init() { + add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 ); + add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 ); + + parent::init(); + + add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 ); + add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs + add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 ); + + // Delete comments count cache whenever there is a new comment or a comment status changes + add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) ); + add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) ); + } + + public function disable_comment_counting() { + wp_defer_comment_counting(true); + } + public function enable_comment_counting() { + wp_defer_comment_counting(false); + } + +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php new file mode 100644 index 0000000000..24c1dffd88 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php @@ -0,0 +1,1066 @@ +<?php + +/** + * Class ActionScheduler_wpPostStore + */ +class ActionScheduler_wpPostStore extends ActionScheduler_Store { + const POST_TYPE = 'scheduled-action'; + const GROUP_TAXONOMY = 'action-group'; + const SCHEDULE_META_KEY = '_action_manager_schedule'; + const DEPENDENCIES_MET = 'as-post-store-dependencies-met'; + + /** + * Used to share information about the before_date property of claims internally. + * + * This is used in preference to passing the same information as a method param + * for backwards-compatibility reasons. + * + * @var DateTime|null + */ + private $claim_before_date = null; + + /** + * Local Timezone. + * + * @var DateTimeZone + */ + protected $local_timezone = null; + + /** + * Save action. + * + * @param ActionScheduler_Action $action Scheduled Action. + * @param DateTime $scheduled_date Scheduled Date. + * + * @throws RuntimeException Throws an exception if the action could not be saved. + * @return int + */ + public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = null ) { + try { + $this->validate_action( $action ); + $post_array = $this->create_post_array( $action, $scheduled_date ); + $post_id = $this->save_post_array( $post_array ); + $this->save_post_schedule( $post_id, $action->get_schedule() ); + $this->save_action_group( $post_id, $action->get_group() ); + do_action( 'action_scheduler_stored_action', $post_id ); + return $post_id; + } catch ( Exception $e ) { + /* translators: %s: action error message */ + throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); + } + } + + /** + * Create post array. + * + * @param ActionScheduler_Action $action Scheduled Action. + * @param DateTime $scheduled_date Scheduled Date. + * + * @return array Returns an array of post data. + */ + protected function create_post_array( ActionScheduler_Action $action, DateTime $scheduled_date = null ) { + $post = array( + 'post_type' => self::POST_TYPE, + 'post_title' => $action->get_hook(), + 'post_content' => wp_json_encode( $action->get_args() ), + 'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ), + 'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ), + 'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ), + ); + return $post; + } + + /** + * Save post array. + * + * @param array $post_array Post array. + * @return int Returns the post ID. + * @throws RuntimeException Throws an exception if the action could not be saved. + */ + protected function save_post_array( $post_array ) { + add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + + $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + kses_remove_filters(); + } + + $post_id = wp_insert_post( $post_array ); + + if ( $has_kses ) { + kses_init_filters(); + } + + remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + + if ( is_wp_error( $post_id ) || empty( $post_id ) ) { + throw new RuntimeException( __( 'Unable to save action.', 'action-scheduler' ) ); + } + return $post_id; + } + + /** + * Filter insert post data. + * + * @param array $postdata Post data to filter. + * + * @return array + */ + public function filter_insert_post_data( $postdata ) { + if ( self::POST_TYPE === $postdata['post_type'] ) { + $postdata['post_author'] = 0; + if ( 'future' === $postdata['post_status'] ) { + $postdata['post_status'] = 'publish'; + } + } + return $postdata; + } + + /** + * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug(). + * + * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish' + * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug() + * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing + * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a + * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a + * database containing thousands of related post_name values. + * + * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue. + * + * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This + * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a + * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an + * action's slug, being probably unique is good enough. + * + * For more backstory on this issue, see: + * - https://github.com/woocommerce/action-scheduler/issues/44 and + * - https://core.trac.wordpress.org/ticket/21112 + * + * @param string $override_slug Short-circuit return value. + * @param string $slug The desired slug (post_name). + * @param int $post_ID Post ID. + * @param string $post_status The post status. + * @param string $post_type Post type. + * @return string + */ + public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) { + if ( self::POST_TYPE === $post_type ) { + $override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false ); + } + return $override_slug; + } + + /** + * Save post schedule. + * + * @param int $post_id Post ID of the scheduled action. + * @param string $schedule Schedule to save. + * + * @return void + */ + protected function save_post_schedule( $post_id, $schedule ) { + update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule ); + } + + /** + * Save action group. + * + * @param int $post_id Post ID. + * @param string $group Group to save. + * @return void + */ + protected function save_action_group( $post_id, $group ) { + if ( empty( $group ) ) { + wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, false ); + } else { + wp_set_object_terms( $post_id, array( $group ), self::GROUP_TAXONOMY, false ); + } + } + + /** + * Fetch actions. + * + * @param int $action_id Action ID. + * @return object + */ + public function fetch_action( $action_id ) { + $post = $this->get_post( $action_id ); + if ( empty( $post ) || self::POST_TYPE !== $post->post_type ) { + return $this->get_null_action(); + } + + try { + $action = $this->make_action_from_post( $post ); + } catch ( ActionScheduler_InvalidActionException $exception ) { + do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception ); + return $this->get_null_action(); + } + + return $action; + } + + /** + * Get post. + * + * @param string $action_id - Action ID. + * @return WP_Post|null + */ + protected function get_post( $action_id ) { + if ( empty( $action_id ) ) { + return null; + } + return get_post( $action_id ); + } + + /** + * Get NULL action. + * + * @return ActionScheduler_NullAction + */ + protected function get_null_action() { + return new ActionScheduler_NullAction(); + } + + /** + * Make action from post. + * + * @param WP_Post $post Post object. + * @return WP_Post + */ + protected function make_action_from_post( $post ) { + $hook = $post->post_title; + + $args = json_decode( $post->post_content, true ); + $this->validate_args( $args, $post->ID ); + + $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true ); + $this->validate_schedule( $schedule, $post->ID ); + + $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array( 'fields' => 'names' ) ); + $group = empty( $group ) ? '' : reset( $group ); + + return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group ); + } + + /** + * Get action status by post status. + * + * @param string $post_status Post status. + * + * @throws InvalidArgumentException Throw InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels(). + * @return string + */ + protected function get_action_status_by_post_status( $post_status ) { + + switch ( $post_status ) { + case 'publish': + $action_status = self::STATUS_COMPLETE; + break; + case 'trash': + $action_status = self::STATUS_CANCELED; + break; + default: + if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) ); + } + $action_status = $post_status; + break; + } + + return $action_status; + } + + /** + * Get post status by action status. + * + * @param string $action_status Action status. + * + * @throws InvalidArgumentException Throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels(). + * @return string + */ + protected function get_post_status_by_action_status( $action_status ) { + + switch ( $action_status ) { + case self::STATUS_COMPLETE: + $post_status = 'publish'; + break; + case self::STATUS_CANCELED: + $post_status = 'trash'; + break; + default: + if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) ); + } + $post_status = $action_status; + break; + } + + return $post_status; + } + + /** + * Returns the SQL statement to query (or count) actions. + * + * @param array $query - Filtering options. + * @param string $select_or_count - Whether the SQL should select and return the IDs or just the row count. + * + * @throws InvalidArgumentException - Throw InvalidArgumentException if $select_or_count not count or select. + * @return string SQL statement. The returned SQL is already properly escaped. + */ + protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { + + if ( ! in_array( $select_or_count, array( 'select', 'count' ), true ) ) { + throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) ); + } + + $query = wp_parse_args( + $query, + array( + 'hook' => '', + 'args' => null, + 'date' => null, + 'date_compare' => '<=', + 'modified' => null, + 'modified_compare' => '<=', + 'group' => '', + 'status' => '', + 'claimed' => null, + 'per_page' => 5, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + 'search' => '', + ) + ); + + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + $sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID '; + $sql .= "FROM {$wpdb->posts} p"; + $sql_params = array(); + if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) { + $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; + $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; + $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; + } elseif ( ! empty( $query['group'] ) ) { + $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; + $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; + $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; + $sql .= ' AND t.slug=%s'; + $sql_params[] = $query['group']; + } + $sql .= ' WHERE post_type=%s'; + $sql_params[] = self::POST_TYPE; + if ( $query['hook'] ) { + $sql .= ' AND p.post_title=%s'; + $sql_params[] = $query['hook']; + } + if ( ! is_null( $query['args'] ) ) { + $sql .= ' AND p.post_content=%s'; + $sql_params[] = wp_json_encode( $query['args'] ); + } + + if ( $query['status'] ) { + $post_statuses = array_map( array( $this, 'get_post_status_by_action_status' ), (array) $query['status'] ); + $placeholders = array_fill( 0, count( $post_statuses ), '%s' ); + $sql .= ' AND p.post_status IN (' . join( ', ', $placeholders ) . ')'; + $sql_params = array_merge( $sql_params, array_values( $post_statuses ) ); + } + + if ( $query['date'] instanceof DateTime ) { + $date = clone $query['date']; + $date->setTimezone( new DateTimeZone( 'UTC' ) ); + $date_string = $date->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query['date_compare'] ); + $sql .= " AND p.post_date_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query['modified'] instanceof DateTime ) { + $modified = clone $query['modified']; + $modified->setTimezone( new DateTimeZone( 'UTC' ) ); + $date_string = $modified->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query['modified_compare'] ); + $sql .= " AND p.post_modified_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( true === $query['claimed'] ) { + $sql .= " AND p.post_password != ''"; + } elseif ( false === $query['claimed'] ) { + $sql .= " AND p.post_password = ''"; + } elseif ( ! is_null( $query['claimed'] ) ) { + $sql .= ' AND p.post_password = %s'; + $sql_params[] = $query['claimed']; + } + + if ( ! empty( $query['search'] ) ) { + $sql .= ' AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)'; + for ( $i = 0; $i < 3; $i++ ) { + $sql_params[] = sprintf( '%%%s%%', $query['search'] ); + } + } + + if ( 'select' === $select_or_count ) { + switch ( $query['orderby'] ) { + case 'hook': + $orderby = 'p.post_title'; + break; + case 'group': + $orderby = 't.name'; + break; + case 'status': + $orderby = 'p.post_status'; + break; + case 'modified': + $orderby = 'p.post_modified'; + break; + case 'claim_id': + $orderby = 'p.post_password'; + break; + case 'schedule': + case 'date': + default: + $orderby = 'p.post_date_gmt'; + break; + } + if ( 'ASC' === strtoupper( $query['order'] ) ) { + $order = 'ASC'; + } else { + $order = 'DESC'; + } + $sql .= " ORDER BY $orderby $order"; + if ( $query['per_page'] > 0 ) { + $sql .= ' LIMIT %d, %d'; + $sql_params[] = $query['offset']; + $sql_params[] = $query['per_page']; + } + } + + return $wpdb->prepare( $sql, $sql_params ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @see ActionScheduler_Store::query_actions for $query arg usage. + * + * @param array $query Query filtering options. + * @param string $query_type Whether to select or count the results. Defaults to select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + public function query_actions( $query = array(), $query_type = 'select' ) { + /** + * Global $wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + $sql = $this->get_query_actions_sql( $query, $query_type ); + + return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array + */ + public function action_counts() { + + $action_counts_by_status = array(); + $action_stati_and_labels = $this->get_status_labels(); + $posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' ); + + foreach ( $posts_count_by_status as $post_status_name => $count ) { + + try { + $action_status_name = $this->get_action_status_by_post_status( $post_status_name ); + } catch ( Exception $e ) { + // Ignore any post statuses that aren't for actions. + continue; + } + if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) { + $action_counts_by_status[ $action_status_name ] = $count; + } + } + + return $action_counts_by_status; + } + + /** + * Cancel action. + * + * @param int $action_id Action ID. + * + * @throws InvalidArgumentException If $action_id is not identified. + */ + public function cancel_action( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { + /* translators: %s is the action ID */ + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + } + do_action( 'action_scheduler_canceled_action', $action_id ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + wp_trash_post( $action_id ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + } + + /** + * Delete action. + * + * @param int $action_id Action ID. + * @return void + * @throws InvalidArgumentException If action is not identified. + */ + public function delete_action( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { + /* translators: %s is the action ID */ + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + } + do_action( 'action_scheduler_deleted_action', $action_id ); + + wp_delete_post( $action_id, true ); + } + + /** + * Get date for claim id. + * + * @param int $action_id Action ID. + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. + */ + public function get_date( $action_id ) { + $next = $this->get_date_gmt( $action_id ); + return ActionScheduler_TimezoneHelper::set_local_timezone( $next ); + } + + /** + * Get Date GMT. + * + * @param int $action_id Action ID. + * + * @throws InvalidArgumentException If $action_id is not identified. + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. + */ + public function get_date_gmt( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { + /* translators: %s is the action ID */ + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + } + if ( 'publish' === $post->post_status ) { + return as_get_datetime_object( $post->post_modified_gmt ); + } else { + return as_get_datetime_object( $post->post_date_gmt ); + } + } + + /** + * Stake claim. + * + * @param int $max_actions Maximum number of actions. + * @param DateTime $before_date Jobs must be schedule before this date. Defaults to now. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return ActionScheduler_ActionClaim + * @throws RuntimeException When there is an error staking a claim. + * @throws InvalidArgumentException When the given group is not valid. + */ + public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) { + $this->claim_before_date = $before_date; + $claim_id = $this->generate_claim_id(); + $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); + $action_ids = $this->find_actions_by_claim_id( $claim_id ); + $this->claim_before_date = null; + + return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); + } + + /** + * Get claim count. + * + * @return int + */ + public function get_claim_count() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')", + array( self::POST_TYPE ) + ) + ); + } + + /** + * Generate claim id. + * + * @return string + */ + protected function generate_claim_id() { + $claim_id = md5( microtime( true ) . wp_rand( 0, 1000 ) ); + return substr( $claim_id, 0, 20 ); // to fit in db field with 20 char limit. + } + + /** + * Claim actions. + * + * @param string $claim_id Claim ID. + * @param int $limit Limit. + * @param DateTime $before_date Should use UTC timezone. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return int The number of actions that were claimed. + * @throws RuntimeException When there is a database error. + */ + protected function claim_actions( $claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '' ) { + // Set up initial variables. + $date = null === $before_date ? as_get_datetime_object() : clone $before_date; + $limit_ids = ! empty( $group ); + $ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array(); + + // If limiting by IDs and no posts found, then return early since we have nothing to update. + if ( $limit_ids && 0 === count( $ids ) ) { + return 0; + } + + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + /* + * Build up custom query to update the affected posts. Parameters are built as a separate array + * to make it easier to identify where they are in the query. + * + * We can't use $wpdb->update() here because of the "ID IN ..." clause. + */ + $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s"; + $params = array( + $claim_id, + current_time( 'mysql', true ), + current_time( 'mysql' ), + ); + + // Build initial WHERE clause. + $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''"; + $params[] = self::POST_TYPE; + $params[] = ActionScheduler_Store::STATUS_PENDING; + + if ( ! empty( $hooks ) ) { + $placeholders = array_fill( 0, count( $hooks ), '%s' ); + $where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')'; + $params = array_merge( $params, array_values( $hooks ) ); + } + + /* + * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query. + * + * If we're not limiting by IDs, then include the post_date_gmt clause. + */ + if ( $limit_ids ) { + $where .= ' AND ID IN (' . join( ',', $ids ) . ')'; + } else { + $where .= ' AND post_date_gmt <= %s'; + $params[] = $date->format( 'Y-m-d H:i:s' ); + } + + // Add the ORDER BY clause and,ms limit. + $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d'; + $params[] = $limit; + + // Run the query and gather results. + $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); // phpcs:ignore // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + + if ( false === $rows_affected ) { + throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); + } + + return (int) $rows_affected; + } + + /** + * Get IDs of actions within a certain group and up to a certain date/time. + * + * @param string $group The group to use in finding actions. + * @param int $limit The number of actions to retrieve. + * @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be + * up to and including this DateTime. + * + * @return array IDs of actions in the appropriate group and before the appropriate time. + * @throws InvalidArgumentException When the group does not exist. + */ + protected function get_actions_by_group( $group, $limit, DateTime $date ) { + // Ensure the group exists before continuing. + if ( ! term_exists( $group, self::GROUP_TAXONOMY ) ) { + /* translators: %s is the group name */ + throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); + } + + // Set up a query for post IDs to use later. + $query = new WP_Query(); + $query_args = array( + 'fields' => 'ids', + 'post_type' => self::POST_TYPE, + 'post_status' => ActionScheduler_Store::STATUS_PENDING, + 'has_password' => false, + 'posts_per_page' => $limit * 3, + 'suppress_filters' => true, + 'no_found_rows' => true, + 'orderby' => array( + 'menu_order' => 'ASC', + 'date' => 'ASC', + 'ID' => 'ASC', + ), + 'date_query' => array( + 'column' => 'post_date_gmt', + 'before' => $date->format( 'Y-m-d H:i' ), + 'inclusive' => true, + ), + 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery + array( + 'taxonomy' => self::GROUP_TAXONOMY, + 'field' => 'slug', + 'terms' => $group, + 'include_children' => false, + ), + ), + ); + + return $query->query( $query_args ); + } + + /** + * Find actions by claim ID. + * + * @param string $claim_id Claim ID. + * @return array + */ + public function find_actions_by_claim_id( $claim_id ) { + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + $action_ids = array(); + $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); + $cut_off = $before_date->format( 'Y-m-d H:i:s' ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s", + array( + self::POST_TYPE, + $claim_id, + ) + ) + ); + + // Verify that the scheduled date for each action is within the expected bounds (in some unusual + // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). + foreach ( $results as $claimed_action ) { + if ( $claimed_action->post_date_gmt <= $cut_off ) { + $action_ids[] = absint( $claimed_action->ID ); + } + } + + return $action_ids; + } + + /** + * Release claim. + * + * @param ActionScheduler_ActionClaim $claim Claim object to release. + * @return void + * @throws RuntimeException When the claim is not unlocked. + */ + public function release_claim( ActionScheduler_ActionClaim $claim ) { + $action_ids = $this->find_actions_by_claim_id( $claim->get_id() ); + if ( empty( $action_ids ) ) { + return; // nothing to do. + } + $action_id_string = implode( ',', array_map( 'intval', $action_ids ) ); + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s", //phpcs:ignore + array( + $claim->get_id(), + ) + ) + ); + if ( false === $result ) { + /* translators: %s: claim ID */ + throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'action-scheduler' ), $claim->get_id() ) ); + } + } + + /** + * Unclaim action. + * + * @param string $action_id Action ID. + * @throws RuntimeException When unable to unlock claim on action ID. + */ + public function unclaim_action( $action_id ) { + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s", + $action_id, + self::POST_TYPE + ) + ); + if ( false === $result ) { + /* translators: %s: action ID */ + throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'action-scheduler' ), $action_id ) ); + } + } + + /** + * Mark failure on action. + * + * @param int $action_id Action ID. + * + * @return void + * @throws RuntimeException When unable to mark failure on action ID. + */ + public function mark_failure( $action_id ) { + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $result = $wpdb->query( + $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s", self::STATUS_FAILED, $action_id, self::POST_TYPE ) + ); + if ( false === $result ) { + /* translators: %s: action ID */ + throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'action-scheduler' ), $action_id ) ); + } + } + + /** + * Return an action's claim ID, as stored in the post password column + * + * @param int $action_id Action ID. + * @return mixed + */ + public function get_claim_id( $action_id ) { + return $this->get_post_column( $action_id, 'post_password' ); + } + + /** + * Return an action's status, as stored in the post status column + * + * @param int $action_id Action ID. + * + * @return mixed + * @throws InvalidArgumentException When the action ID is invalid. + */ + public function get_status( $action_id ) { + $status = $this->get_post_column( $action_id, 'post_status' ); + + if ( null === $status ) { + throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); + } + + return $this->get_action_status_by_post_status( $status ); + } + + /** + * Get post column + * + * @param string $action_id Action ID. + * @param string $column_name Column Name. + * + * @return string|null + */ + private function get_post_column( $action_id, $column_name ) { + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_var( + $wpdb->prepare( + "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", // phpcs:ignore + $action_id, + self::POST_TYPE + ) + ); + } + + /** + * Log Execution. + * + * @param string $action_id Action ID. + */ + public function log_execution( $action_id ) { + /** + * Global wpdb object. + * + * @var wpdb $wpdb + */ + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s", + self::STATUS_RUNNING, + current_time( 'mysql', true ), + current_time( 'mysql' ), + $action_id, + self::POST_TYPE + ) + ); + } + + /** + * Record that an action was completed. + * + * @param string $action_id ID of the completed action. + * + * @throws InvalidArgumentException When the action ID is invalid. + * @throws RuntimeException When there was an error executing the action. + */ + public function mark_complete( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( self::POST_TYPE !== $post->post_type ) ) { + /* translators: %s is the action ID */ + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); + } + add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + $result = wp_update_post( + array( + 'ID' => $action_id, + 'post_status' => 'publish', + ), + true + ); + remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + if ( is_wp_error( $result ) ) { + throw new RuntimeException( $result->get_error_message() ); + } + } + + /** + * Mark action as migrated when there is an error deleting the action. + * + * @param int $action_id Action ID. + */ + public function mark_migrated( $action_id ) { + wp_update_post( + array( + 'ID' => $action_id, + 'post_status' => 'migrated', + ) + ); + } + + /** + * Determine whether the post store can be migrated. + * + * @param [type] $setting - Setting value. + * @return bool + */ + public function migration_dependencies_met( $setting ) { + global $wpdb; + + $dependencies_met = get_transient( self::DEPENDENCIES_MET ); + if ( empty( $dependencies_met ) ) { + $maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 ); + $found_action = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1", + $maximum_args_length, + self::POST_TYPE + ) + ); + $dependencies_met = $found_action ? 'no' : 'yes'; + set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS ); + } + + return 'yes' === $dependencies_met ? $setting : false; + } + + /** + * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. + * + * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, + * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn + * developers of this impending requirement. + * + * @param ActionScheduler_Action $action Action object. + */ + protected function validate_action( ActionScheduler_Action $action ) { + try { + parent::validate_action( $action ); + } catch ( Exception $e ) { + /* translators: %s is the error message */ + $message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'action-scheduler' ), $e->getMessage() ); + _doing_it_wrong( 'ActionScheduler_Action::$args', esc_html( $message ), '2.1.0' ); + } + } + + /** + * (@codeCoverageIgnore) + */ + public function init() { + add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) ); + + $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar(); + $post_type_registrar->register(); + + $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar(); + $post_status_registrar->register(); + + $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar(); + $taxonomy_registrar->register(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php new file mode 100644 index 0000000000..246bc347bf --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php @@ -0,0 +1,58 @@ +<?php + +/** + * Class ActionScheduler_wpPostStore_PostStatusRegistrar + * @codeCoverageIgnore + */ +class ActionScheduler_wpPostStore_PostStatusRegistrar { + public function register() { + register_post_status( ActionScheduler_Store::STATUS_RUNNING, array_merge( $this->post_status_args(), $this->post_status_running_labels() ) ); + register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_args() { + $args = array( + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + ); + + return apply_filters( 'action_scheduler_post_status_args', $args ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_failed_labels() { + $labels = array( + 'label' => _x( 'Failed', 'post', 'action-scheduler' ), + /* translators: %s: count */ + 'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'action-scheduler' ), + ); + + return apply_filters( 'action_scheduler_post_status_failed_labels', $labels ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_running_labels() { + $labels = array( + 'label' => _x( 'In-Progress', 'post', 'action-scheduler' ), + /* translators: %s: count */ + 'label_count' => _n_noop( 'In-Progress <span class="count">(%s)</span>', 'In-Progress <span class="count">(%s)</span>', 'action-scheduler' ), + ); + + return apply_filters( 'action_scheduler_post_status_running_labels', $labels ); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php new file mode 100644 index 0000000000..8c63bd0f7a --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php @@ -0,0 +1,50 @@ +<?php + +/** + * Class ActionScheduler_wpPostStore_PostTypeRegistrar + * @codeCoverageIgnore + */ +class ActionScheduler_wpPostStore_PostTypeRegistrar { + public function register() { + register_post_type( ActionScheduler_wpPostStore::POST_TYPE, $this->post_type_args() ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_type_args() { + $args = array( + 'label' => __( 'Scheduled Actions', 'action-scheduler' ), + 'description' => __( 'Scheduled actions are hooks triggered on a cetain date and time.', 'action-scheduler' ), + 'public' => false, + 'map_meta_cap' => true, + 'hierarchical' => false, + 'supports' => array('title', 'editor','comments'), + 'rewrite' => false, + 'query_var' => false, + 'can_export' => true, + 'ep_mask' => EP_NONE, + 'labels' => array( + 'name' => __( 'Scheduled Actions', 'action-scheduler' ), + 'singular_name' => __( 'Scheduled Action', 'action-scheduler' ), + 'menu_name' => _x( 'Scheduled Actions', 'Admin menu name', 'action-scheduler' ), + 'add_new' => __( 'Add', 'action-scheduler' ), + 'add_new_item' => __( 'Add New Scheduled Action', 'action-scheduler' ), + 'edit' => __( 'Edit', 'action-scheduler' ), + 'edit_item' => __( 'Edit Scheduled Action', 'action-scheduler' ), + 'new_item' => __( 'New Scheduled Action', 'action-scheduler' ), + 'view' => __( 'View Action', 'action-scheduler' ), + 'view_item' => __( 'View Action', 'action-scheduler' ), + 'search_items' => __( 'Search Scheduled Actions', 'action-scheduler' ), + 'not_found' => __( 'No actions found', 'action-scheduler' ), + 'not_found_in_trash' => __( 'No actions found in trash', 'action-scheduler' ), + ), + ); + + $args = apply_filters('action_scheduler_post_type_args', $args); + return $args; + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php new file mode 100644 index 0000000000..367401f7e2 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php @@ -0,0 +1,26 @@ +<?php + +/** + * Class ActionScheduler_wpPostStore_TaxonomyRegistrar + * @codeCoverageIgnore + */ +class ActionScheduler_wpPostStore_TaxonomyRegistrar { + public function register() { + register_taxonomy( ActionScheduler_wpPostStore::GROUP_TAXONOMY, ActionScheduler_wpPostStore::POST_TYPE, $this->taxonomy_args() ); + } + + protected function taxonomy_args() { + $args = array( + 'label' => __( 'Action Group', 'action-scheduler' ), + 'public' => false, + 'hierarchical' => false, + 'show_admin_column' => true, + 'query_var' => false, + 'rewrite' => false, + ); + + $args = apply_filters( 'action_scheduler_taxonomy_args', $args ); + return $args; + } +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/migration/ActionMigrator.php b/vendor/woocommerce/action-scheduler/classes/migration/ActionMigrator.php new file mode 100644 index 0000000000..c77d0832cd --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/ActionMigrator.php @@ -0,0 +1,109 @@ +<?php + + +namespace Action_Scheduler\Migration; + +/** + * Class ActionMigrator + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class ActionMigrator { + /** var ActionScheduler_Store */ + private $source; + + /** var ActionScheduler_Store */ + private $destination; + + /** var LogMigrator */ + private $log_migrator; + + /** + * ActionMigrator constructor. + * + * @param ActionScheduler_Store $source_store Source store object. + * @param ActionScheduler_Store $destination_store Destination store object. + * @param LogMigrator $log_migrator Log migrator object. + */ + public function __construct( \ActionScheduler_Store $source_store, \ActionScheduler_Store $destination_store, LogMigrator $log_migrator ) { + $this->source = $source_store; + $this->destination = $destination_store; + $this->log_migrator = $log_migrator; + } + + /** + * Migrate an action. + * + * @param int $source_action_id Action ID. + * + * @return int 0|new action ID + */ + public function migrate( $source_action_id ) { + try { + $action = $this->source->fetch_action( $source_action_id ); + $status = $this->source->get_status( $source_action_id ); + } catch ( \Exception $e ) { + $action = null; + $status = ''; + } + + if ( is_null( $action ) || empty( $status ) || ! $action->get_schedule()->get_date() ) { + // null action or empty status means the fetch operation failed or the action didn't exist + // null schedule means it's missing vital data + // delete it and move on + try { + $this->source->delete_action( $source_action_id ); + } catch ( \Exception $e ) { + // nothing to do, it didn't exist in the first place + } + do_action( 'action_scheduler/no_action_to_migrate', $source_action_id, $this->source, $this->destination ); + + return 0; + } + + try { + + // Make sure the last attempt date is set correctly for completed and failed actions + $last_attempt_date = ( $status !== \ActionScheduler_Store::STATUS_PENDING ) ? $this->source->get_date( $source_action_id ) : null; + + $destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date ); + } catch ( \Exception $e ) { + do_action( 'action_scheduler/migrate_action_failed', $source_action_id, $this->source, $this->destination ); + + return 0; // could not save the action in the new store + } + + try { + switch ( $status ) { + case \ActionScheduler_Store::STATUS_FAILED : + $this->destination->mark_failure( $destination_action_id ); + break; + case \ActionScheduler_Store::STATUS_CANCELED : + $this->destination->cancel_action( $destination_action_id ); + break; + } + + $this->log_migrator->migrate( $source_action_id, $destination_action_id ); + $this->source->delete_action( $source_action_id ); + + $test_action = $this->source->fetch_action( $source_action_id ); + if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) { + throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'action-scheduler' ), $source_action_id ) ); + } + do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); + + return $destination_action_id; + } catch ( \Exception $e ) { + // could not delete from the old store + $this->source->mark_migrated( $source_action_id ); + do_action( 'action_scheduler/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination ); + do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); + + return $destination_action_id; + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php b/vendor/woocommerce/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php new file mode 100644 index 0000000000..41c21da256 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php @@ -0,0 +1,47 @@ +<?php + +/** + * Class ActionScheduler_DBStoreMigrator + * + * A class for direct saving of actions to the table data store during migration. + * + * @since 3.0.0 + */ +class ActionScheduler_DBStoreMigrator extends ActionScheduler_DBStore { + + /** + * Save an action with optional last attempt date. + * + * Normally, saving an action sets its attempted date to 0000-00-00 00:00:00 because when an action is first saved, + * it can't have been attempted yet, but migrated completed actions will have an attempted date, so we need to save + * that when first saving the action. + * + * @param ActionScheduler_Action $action + * @param \DateTime $scheduled_date Optional date of the first instance to store. + * @param \DateTime $last_attempt_date Optional date the action was last attempted. + * + * @return string The action ID + * @throws \RuntimeException When the action is not saved. + */ + public function save_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null, \DateTime $last_attempt_date = null ){ + try { + /** @var \wpdb $wpdb */ + global $wpdb; + + $action_id = parent::save_action( $action, $scheduled_date ); + + if ( null !== $last_attempt_date ) { + $data = [ + 'last_attempt_gmt' => $this->get_scheduled_date_string( $action, $last_attempt_date ), + 'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ), + ]; + + $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) ); + } + + return $action_id; + } catch ( \Exception $e ) { + throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/BatchFetcher.php b/vendor/woocommerce/action-scheduler/classes/migration/BatchFetcher.php new file mode 100644 index 0000000000..48728010fe --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/BatchFetcher.php @@ -0,0 +1,86 @@ +<?php + + +namespace Action_Scheduler\Migration; + + +use ActionScheduler_Store as Store; + +/** + * Class BatchFetcher + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class BatchFetcher { + /** var ActionScheduler_Store */ + private $store; + + /** + * BatchFetcher constructor. + * + * @param ActionScheduler_Store $source_store Source store object. + */ + public function __construct( Store $source_store ) { + $this->store = $source_store; + } + + /** + * Retrieve a list of actions. + * + * @param int $count The number of actions to retrieve + * + * @return int[] A list of action IDs + */ + public function fetch( $count = 10 ) { + foreach ( $this->get_query_strategies( $count ) as $query ) { + $action_ids = $this->store->query_actions( $query ); + if ( ! empty( $action_ids ) ) { + return $action_ids; + } + } + + return []; + } + + /** + * Generate a list of prioritized of action search parameters. + * + * @param int $count Number of actions to find. + * + * @return array + */ + private function get_query_strategies( $count ) { + $now = as_get_datetime_object(); + $args = [ + 'date' => $now, + 'per_page' => $count, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + ]; + + $priorities = [ + Store::STATUS_PENDING, + Store::STATUS_FAILED, + Store::STATUS_CANCELED, + Store::STATUS_COMPLETE, + Store::STATUS_RUNNING, + '', // any other unanticipated status + ]; + + foreach ( $priorities as $status ) { + yield wp_parse_args( [ + 'status' => $status, + 'date_compare' => '<=', + ], $args ); + yield wp_parse_args( [ + 'status' => $status, + 'date_compare' => '>=', + ], $args ); + } + } +} \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/migration/Config.php b/vendor/woocommerce/action-scheduler/classes/migration/Config.php new file mode 100644 index 0000000000..50f41ff49a --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/Config.php @@ -0,0 +1,168 @@ +<?php + + +namespace Action_Scheduler\Migration; + +use Action_Scheduler\WP_CLI\ProgressBar; +use ActionScheduler_Logger as Logger; +use ActionScheduler_Store as Store; + +/** + * Class Config + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * A config builder for the ActionScheduler\Migration\Runner class + */ +class Config { + /** @var ActionScheduler_Store */ + private $source_store; + + /** @var ActionScheduler_Logger */ + private $source_logger; + + /** @var ActionScheduler_Store */ + private $destination_store; + + /** @var ActionScheduler_Logger */ + private $destination_logger; + + /** @var Progress bar */ + private $progress_bar; + + /** @var bool */ + private $dry_run = false; + + /** + * Config constructor. + */ + public function __construct() { + + } + + /** + * Get the configured source store. + * + * @return ActionScheduler_Store + */ + public function get_source_store() { + if ( empty( $this->source_store ) ) { + throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'action-scheduler' ) ); + } + + return $this->source_store; + } + + /** + * Set the configured source store. + * + * @param ActionScheduler_Store $store Source store object. + */ + public function set_source_store( Store $store ) { + $this->source_store = $store; + } + + /** + * Get the configured source loger. + * + * @return ActionScheduler_Logger + */ + public function get_source_logger() { + if ( empty( $this->source_logger ) ) { + throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'action-scheduler' ) ); + } + + return $this->source_logger; + } + + /** + * Set the configured source logger. + * + * @param ActionScheduler_Logger $logger + */ + public function set_source_logger( Logger $logger ) { + $this->source_logger = $logger; + } + + /** + * Get the configured destination store. + * + * @return ActionScheduler_Store + */ + public function get_destination_store() { + if ( empty( $this->destination_store ) ) { + throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'action-scheduler' ) ); + } + + return $this->destination_store; + } + + /** + * Set the configured destination store. + * + * @param ActionScheduler_Store $store + */ + public function set_destination_store( Store $store ) { + $this->destination_store = $store; + } + + /** + * Get the configured destination logger. + * + * @return ActionScheduler_Logger + */ + public function get_destination_logger() { + if ( empty( $this->destination_logger ) ) { + throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'action-scheduler' ) ); + } + + return $this->destination_logger; + } + + /** + * Set the configured destination logger. + * + * @param ActionScheduler_Logger $logger + */ + public function set_destination_logger( Logger $logger ) { + $this->destination_logger = $logger; + } + + /** + * Get flag indicating whether it's a dry run. + * + * @return bool + */ + public function get_dry_run() { + return $this->dry_run; + } + + /** + * Set flag indicating whether it's a dry run. + * + * @param bool $dry_run + */ + public function set_dry_run( $dry_run ) { + $this->dry_run = (bool) $dry_run; + } + + /** + * Get progress bar object. + * + * @return ActionScheduler\WPCLI\ProgressBar + */ + public function get_progress_bar() { + return $this->progress_bar; + } + + /** + * Set progress bar object. + * + * @param ActionScheduler\WPCLI\ProgressBar $progress_bar + */ + public function set_progress_bar( ProgressBar $progress_bar ) { + $this->progress_bar = $progress_bar; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/Controller.php b/vendor/woocommerce/action-scheduler/classes/migration/Controller.php new file mode 100644 index 0000000000..b2b618d8f2 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/Controller.php @@ -0,0 +1,226 @@ +<?php + +namespace Action_Scheduler\Migration; + +use ActionScheduler_DataController; +use ActionScheduler_LoggerSchema; +use ActionScheduler_StoreSchema; +use Action_Scheduler\WP_CLI\ProgressBar; + +/** + * Class Controller + * + * The main plugin/initialization class for migration to custom tables. + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class Controller { + private static $instance; + + /** @var Action_Scheduler\Migration\Scheduler */ + private $migration_scheduler; + + /** @var string */ + private $store_classname; + + /** @var string */ + private $logger_classname; + + /** @var bool */ + private $migrate_custom_store; + + /** + * Controller constructor. + * + * @param Scheduler $migration_scheduler Migration scheduler object. + */ + protected function __construct( Scheduler $migration_scheduler ) { + $this->migration_scheduler = $migration_scheduler; + $this->store_classname = ''; + } + + /** + * Set the action store class name. + * + * @param string $class Classname of the store class. + * + * @return string + */ + public function get_store_class( $class ) { + if ( \ActionScheduler_DataController::is_migration_complete() ) { + return \ActionScheduler_DataController::DATASTORE_CLASS; + } elseif ( \ActionScheduler_Store::DEFAULT_CLASS !== $class ) { + $this->store_classname = $class; + return $class; + } else { + return 'ActionScheduler_HybridStore'; + } + } + + /** + * Set the action logger class name. + * + * @param string $class Classname of the logger class. + * + * @return string + */ + public function get_logger_class( $class ) { + \ActionScheduler_Store::instance(); + + if ( $this->has_custom_datastore() ) { + $this->logger_classname = $class; + return $class; + } else { + return \ActionScheduler_DataController::LOGGER_CLASS; + } + } + + /** + * Get flag indicating whether a custom datastore is in use. + * + * @return bool + */ + public function has_custom_datastore() { + return (bool) $this->store_classname; + } + + /** + * Set up the background migration process. + * + * @return void + */ + public function schedule_migration() { + $logging_tables = new ActionScheduler_LoggerSchema(); + $store_tables = new ActionScheduler_StoreSchema(); + + /* + * In some unusual cases, the expected tables may not have been created. In such cases + * we do not schedule a migration as doing so will lead to fatal error conditions. + * + * In such cases the user will likely visit the Tools > Scheduled Actions screen to + * investigate, and will see appropriate messaging (this step also triggers an attempt + * to rebuild any missing tables). + * + * @see https://github.com/woocommerce/action-scheduler/issues/653 + */ + if ( + ActionScheduler_DataController::is_migration_complete() + || $this->migration_scheduler->is_migration_scheduled() + || ! $store_tables->tables_exist() + || ! $logging_tables->tables_exist() + ) { + return; + } + + $this->migration_scheduler->schedule_migration(); + } + + /** + * Get the default migration config object + * + * @return ActionScheduler\Migration\Config + */ + public function get_migration_config_object() { + static $config = null; + + if ( ! $config ) { + $source_store = $this->store_classname ? new $this->store_classname() : new \ActionScheduler_wpPostStore(); + $source_logger = $this->logger_classname ? new $this->logger_classname() : new \ActionScheduler_wpCommentLogger(); + + $config = new Config(); + $config->set_source_store( $source_store ); + $config->set_source_logger( $source_logger ); + $config->set_destination_store( new \ActionScheduler_DBStoreMigrator() ); + $config->set_destination_logger( new \ActionScheduler_DBLogger() ); + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + $config->set_progress_bar( new ProgressBar( '', 0 ) ); + } + } + + return apply_filters( 'action_scheduler/migration_config', $config ); + } + + /** + * Hook dashboard migration notice. + */ + public function hook_admin_notices() { + if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { + return; + } + add_action( 'admin_notices', array( $this, 'display_migration_notice' ), 10, 0 ); + } + + /** + * Show a dashboard notice that migration is in progress. + */ + public function display_migration_notice() { + printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html__( 'Action Scheduler migration in progress. The list of scheduled actions may be incomplete.', 'action-scheduler' ) ); + } + + /** + * Add store classes. Hook migration. + */ + private function hook() { + add_filter( 'action_scheduler_store_class', array( $this, 'get_store_class' ), 100, 1 ); + add_filter( 'action_scheduler_logger_class', array( $this, 'get_logger_class' ), 100, 1 ); + add_action( 'init', array( $this, 'maybe_hook_migration' ) ); + add_action( 'wp_loaded', array( $this, 'schedule_migration' ) ); + + // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen + add_action( 'load-tools_page_action-scheduler', array( $this, 'hook_admin_notices' ), 10, 0 ); + add_action( 'load-woocommerce_page_wc-status', array( $this, 'hook_admin_notices' ), 10, 0 ); + } + + /** + * Possibly hook the migration scheduler action. + * + * @author Jeremy Pry + */ + public function maybe_hook_migration() { + if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { + return; + } + + $this->migration_scheduler->hook(); + } + + /** + * Allow datastores to enable migration to AS tables. + */ + public function allow_migration() { + if ( ! \ActionScheduler_DataController::dependencies_met() ) { + return false; + } + + if ( null === $this->migrate_custom_store ) { + $this->migrate_custom_store = apply_filters( 'action_scheduler_migrate_data_store', false ); + } + + return ( ! $this->has_custom_datastore() ) || $this->migrate_custom_store; + } + + /** + * Proceed with the migration if the dependencies have been met. + */ + public static function init() { + if ( \ActionScheduler_DataController::dependencies_met() ) { + self::instance()->hook(); + } + } + + /** + * Singleton factory. + */ + public static function instance() { + if ( ! isset( self::$instance ) ) { + self::$instance = new static( new Scheduler() ); + } + + return self::$instance; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/DryRun_ActionMigrator.php b/vendor/woocommerce/action-scheduler/classes/migration/DryRun_ActionMigrator.php new file mode 100644 index 0000000000..ffc21c2875 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/DryRun_ActionMigrator.php @@ -0,0 +1,28 @@ +<?php + + +namespace Action_Scheduler\Migration; + +/** + * Class DryRun_ActionMigrator + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class DryRun_ActionMigrator extends ActionMigrator { + /** + * Simulate migrating an action. + * + * @param int $source_action_id Action ID. + * + * @return int + */ + public function migrate( $source_action_id ) { + do_action( 'action_scheduler/migrate_action_dry_run', $source_action_id ); + + return 0; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/DryRun_LogMigrator.php b/vendor/woocommerce/action-scheduler/classes/migration/DryRun_LogMigrator.php new file mode 100644 index 0000000000..fc9e4d3d69 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/DryRun_LogMigrator.php @@ -0,0 +1,23 @@ +<?php + + +namespace Action_Scheduler\Migration; + +/** + * Class DryRun_LogMigrator + * + * @package Action_Scheduler\Migration + * + * @codeCoverageIgnore + */ +class DryRun_LogMigrator extends LogMigrator { + /** + * Simulate migrating an action log. + * + * @param int $source_action_id Source logger object. + * @param int $destination_action_id Destination logger object. + */ + public function migrate( $source_action_id, $destination_action_id ) { + // no-op + } +} \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/migration/LogMigrator.php b/vendor/woocommerce/action-scheduler/classes/migration/LogMigrator.php new file mode 100644 index 0000000000..b85da8550b --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/LogMigrator.php @@ -0,0 +1,49 @@ +<?php + + +namespace Action_Scheduler\Migration; + +use ActionScheduler_Logger; + +/** + * Class LogMigrator + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class LogMigrator { + /** @var ActionScheduler_Logger */ + private $source; + + /** @var ActionScheduler_Logger */ + private $destination; + + /** + * ActionMigrator constructor. + * + * @param ActionScheduler_Logger $source_logger Source logger object. + * @param ActionScheduler_Logger $destination_Logger Destination logger object. + */ + public function __construct( ActionScheduler_Logger $source_logger, ActionScheduler_Logger $destination_Logger ) { + $this->source = $source_logger; + $this->destination = $destination_Logger; + } + + /** + * Migrate an action log. + * + * @param int $source_action_id Source logger object. + * @param int $destination_action_id Destination logger object. + */ + public function migrate( $source_action_id, $destination_action_id ) { + $logs = $this->source->get_logs( $source_action_id ); + foreach ( $logs as $log ) { + if ( $log->get_action_id() == $source_action_id ) { + $this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() ); + } + } + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/Runner.php b/vendor/woocommerce/action-scheduler/classes/migration/Runner.php new file mode 100644 index 0000000000..867c5de681 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/Runner.php @@ -0,0 +1,136 @@ +<?php + + +namespace Action_Scheduler\Migration; + +/** + * Class Runner + * + * @package Action_Scheduler\Migration + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class Runner { + /** @var ActionScheduler_Store */ + private $source_store; + + /** @var ActionScheduler_Store */ + private $destination_store; + + /** @var ActionScheduler_Logger */ + private $source_logger; + + /** @var ActionScheduler_Logger */ + private $destination_logger; + + /** @var BatchFetcher */ + private $batch_fetcher; + + /** @var ActionMigrator */ + private $action_migrator; + + /** @var LogMigrator */ + private $log_migrator; + + /** @var ProgressBar */ + private $progress_bar; + + /** + * Runner constructor. + * + * @param Config $config Migration configuration object. + */ + public function __construct( Config $config ) { + $this->source_store = $config->get_source_store(); + $this->destination_store = $config->get_destination_store(); + $this->source_logger = $config->get_source_logger(); + $this->destination_logger = $config->get_destination_logger(); + + $this->batch_fetcher = new BatchFetcher( $this->source_store ); + if ( $config->get_dry_run() ) { + $this->log_migrator = new DryRun_LogMigrator( $this->source_logger, $this->destination_logger ); + $this->action_migrator = new DryRun_ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); + } else { + $this->log_migrator = new LogMigrator( $this->source_logger, $this->destination_logger ); + $this->action_migrator = new ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); + } + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + $this->progress_bar = $config->get_progress_bar(); + } + } + + /** + * Run migration batch. + * + * @param int $batch_size Optional batch size. Default 10. + * + * @return int Size of batch processed. + */ + public function run( $batch_size = 10 ) { + $batch = $this->batch_fetcher->fetch( $batch_size ); + $batch_size = count( $batch ); + + if ( ! $batch_size ) { + return 0; + } + + if ( $this->progress_bar ) { + /* translators: %d: amount of actions */ + $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), number_format_i18n( $batch_size ) ) ); + $this->progress_bar->set_count( $batch_size ); + } + + $this->migrate_actions( $batch ); + + return $batch_size; + } + + /** + * Migration a batch of actions. + * + * @param array $action_ids List of action IDs to migrate. + */ + public function migrate_actions( array $action_ids ) { + do_action( 'action_scheduler/migration_batch_starting', $action_ids ); + + \ActionScheduler::logger()->unhook_stored_action(); + $this->destination_logger->unhook_stored_action(); + + foreach ( $action_ids as $source_action_id ) { + $destination_action_id = $this->action_migrator->migrate( $source_action_id ); + if ( $destination_action_id ) { + $this->destination_logger->log( $destination_action_id, sprintf( + /* translators: 1: source action ID 2: source store class 3: destination action ID 4: destination store class */ + __( 'Migrated action with ID %1$d in %2$s to ID %3$d in %4$s', 'action-scheduler' ), + $source_action_id, + get_class( $this->source_store ), + $destination_action_id, + get_class( $this->destination_store ) + ) ); + } + + if ( $this->progress_bar ) { + $this->progress_bar->tick(); + } + } + + if ( $this->progress_bar ) { + $this->progress_bar->finish(); + } + + \ActionScheduler::logger()->hook_stored_action(); + + do_action( 'action_scheduler/migration_batch_complete', $action_ids ); + } + + /** + * Initialize destination store and logger. + */ + public function init_destination() { + $this->destination_store->init(); + $this->destination_logger->init(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/migration/Scheduler.php b/vendor/woocommerce/action-scheduler/classes/migration/Scheduler.php new file mode 100644 index 0000000000..dcbe2db5fa --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/migration/Scheduler.php @@ -0,0 +1,128 @@ +<?php + + +namespace Action_Scheduler\Migration; + +/** + * Class Scheduler + * + * @package Action_Scheduler\WP_CLI + * + * @since 3.0.0 + * + * @codeCoverageIgnore + */ +class Scheduler { + /** Migration action hook. */ + const HOOK = 'action_scheduler/migration_hook'; + + /** Migration action group. */ + const GROUP = 'action-scheduler-migration'; + + /** + * Set up the callback for the scheduled job. + */ + public function hook() { + add_action( self::HOOK, array( $this, 'run_migration' ), 10, 0 ); + } + + /** + * Remove the callback for the scheduled job. + */ + public function unhook() { + remove_action( self::HOOK, array( $this, 'run_migration' ), 10 ); + } + + /** + * The migration callback. + */ + public function run_migration() { + $migration_runner = $this->get_migration_runner(); + $count = $migration_runner->run( $this->get_batch_size() ); + + if ( $count === 0 ) { + $this->mark_complete(); + } else { + $this->schedule_migration( time() + $this->get_schedule_interval() ); + } + } + + /** + * Mark the migration complete. + */ + public function mark_complete() { + $this->unschedule_migration(); + + \ActionScheduler_DataController::mark_migration_complete(); + do_action( 'action_scheduler/migration_complete' ); + } + + /** + * Get a flag indicating whether the migration is scheduled. + * + * @return bool Whether there is a pending action in the store to handle the migration + */ + public function is_migration_scheduled() { + $next = as_next_scheduled_action( self::HOOK ); + + return ! empty( $next ); + } + + /** + * Schedule the migration. + * + * @param int $when Optional timestamp to run the next migration batch. Defaults to now. + * + * @return string The action ID + */ + public function schedule_migration( $when = 0 ) { + $next = as_next_scheduled_action( self::HOOK ); + + if ( ! empty( $next ) ) { + return $next; + } + + if ( empty( $when ) ) { + $when = time() + MINUTE_IN_SECONDS; + } + + return as_schedule_single_action( $when, self::HOOK, array(), self::GROUP ); + } + + /** + * Remove the scheduled migration action. + */ + public function unschedule_migration() { + as_unschedule_action( self::HOOK, null, self::GROUP ); + } + + /** + * Get migration batch schedule interval. + * + * @return int Seconds between migration runs. Defaults to 0 seconds to allow chaining migration via Async Runners. + */ + private function get_schedule_interval() { + return (int) apply_filters( 'action_scheduler/migration_interval', 0 ); + } + + /** + * Get migration batch size. + * + * @return int Number of actions to migrate in each batch. Defaults to 250. + */ + private function get_batch_size() { + return (int) apply_filters( 'action_scheduler/migration_batch_size', 250 ); + } + + /** + * Get migration runner object. + * + * @return Runner + */ + private function get_migration_runner() { + $config = Controller::instance()->get_migration_config_object(); + + return new Runner( $config ); + } + +} diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php new file mode 100644 index 0000000000..840e482c15 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php @@ -0,0 +1,57 @@ +<?php + +/** + * Class ActionScheduler_SimpleSchedule + */ +class ActionScheduler_CanceledSchedule extends ActionScheduler_SimpleSchedule { + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $timestamp = NULL; + + /** + * @param DateTime $after + * + * @return DateTime|null + */ + public function calculate_next( DateTime $after ) { + return null; + } + + /** + * Cancelled actions should never have a next schedule, even if get_next() + * is called with $after < $this->scheduled_date. + * + * @param DateTime $after + * @return DateTime|null + */ + public function get_next( DateTime $after ) { + return null; + } + + /** + * @return bool + */ + public function is_recurring() { + return false; + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To maintain backward + * compatibility with schedules serialized and stored prior to 3.0, we need to correctly + * map the old property names with matching visibility. + */ + public function __wakeup() { + if ( ! is_null( $this->timestamp ) ) { + $this->scheduled_timestamp = $this->timestamp; + unset( $this->timestamp ); + } + parent::__wakeup(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php new file mode 100644 index 0000000000..7859307ac8 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php @@ -0,0 +1,102 @@ +<?php + +/** + * Class ActionScheduler_CronSchedule + */ +class ActionScheduler_CronSchedule extends ActionScheduler_Abstract_RecurringSchedule implements ActionScheduler_Schedule { + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $start_timestamp = NULL; + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $cron = NULL; + + /** + * Wrapper for parent constructor to accept a cron expression string and map it to a CronExpression for this + * objects $recurrence property. + * + * @param DateTime $start The date & time to run the action at or after. If $start aligns with the CronSchedule passed via $recurrence, it will be used. If it does not align, the first matching date after it will be used. + * @param CronExpression|string $recurrence The CronExpression used to calculate the schedule's next instance. + * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. + */ + public function __construct( DateTime $start, $recurrence, DateTime $first = null ) { + if ( ! is_a( $recurrence, 'CronExpression' ) ) { + $recurrence = CronExpression::factory( $recurrence ); + } + + // For backward compatibility, we need to make sure the date is set to the first matching cron date, not whatever date is passed in. Importantly, by passing true as the 3rd param, if $start matches the cron expression, then it will be used. This was previously handled in the now deprecated next() method. + $date = $recurrence->getNextRunDate( $start, 0, true ); + + // parent::__construct() will set this to $date by default, but that may be different to $start now. + $first = empty( $first ) ? $start : $first; + + parent::__construct( $date, $recurrence, $first ); + } + + /** + * Calculate when an instance of this schedule would start based on a given + * date & time using its the CronExpression. + * + * @param DateTime $after + * @return DateTime + */ + protected function calculate_next( DateTime $after ) { + return $this->recurrence->getNextRunDate( $after, 0, false ); + } + + /** + * @return string + */ + public function get_recurrence() { + return strval( $this->recurrence ); + } + + /** + * Serialize cron schedules with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to + * also store the data with the old property names so if it's unserialized in AS < 3.0, + * the schedule doesn't end up with a null recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->start_timestamp = $this->scheduled_timestamp; + $this->cron = $this->recurrence; + + return array_merge( $sleep_params, array( + 'start_timestamp', + 'cron' + ) ); + } + + /** + * Unserialize cron schedules serialized/stored prior to AS 3.0.0 + * + * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). + */ + public function __wakeup() { + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { + $this->scheduled_timestamp = $this->start_timestamp; + unset( $this->start_timestamp ); + } + + if ( is_null( $this->recurrence ) && ! is_null( $this->cron ) ) { + $this->recurrence = $this->cron; + unset( $this->cron ); + } + parent::__wakeup(); + } +} + diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php new file mode 100644 index 0000000000..11a591e80b --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php @@ -0,0 +1,81 @@ +<?php + +/** + * Class ActionScheduler_IntervalSchedule + */ +class ActionScheduler_IntervalSchedule extends ActionScheduler_Abstract_RecurringSchedule implements ActionScheduler_Schedule { + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $start_timestamp = NULL; + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $interval_in_seconds = NULL; + + /** + * Calculate when this schedule should start after a given date & time using + * the number of seconds between recurrences. + * + * @param DateTime $after + * @return DateTime + */ + protected function calculate_next( DateTime $after ) { + $after->modify( '+' . (int) $this->get_recurrence() . ' seconds' ); + return $after; + } + + /** + * @return int + */ + public function interval_in_seconds() { + _deprecated_function( __METHOD__, '3.0.0', '(int)ActionScheduler_Abstract_RecurringSchedule::get_recurrence()' ); + return (int) $this->get_recurrence(); + } + + /** + * Serialize interval schedules with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to + * also store the data with the old property names so if it's unserialized in AS < 3.0, + * the schedule doesn't end up with a null/false/0 recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->start_timestamp = $this->scheduled_timestamp; + $this->interval_in_seconds = $this->recurrence; + + return array_merge( $sleep_params, array( + 'start_timestamp', + 'interval_in_seconds' + ) ); + } + + /** + * Unserialize interval schedules serialized/stored prior to AS 3.0.0 + * + * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). + */ + public function __wakeup() { + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { + $this->scheduled_timestamp = $this->start_timestamp; + unset( $this->start_timestamp ); + } + + if ( is_null( $this->recurrence ) && ! is_null( $this->interval_in_seconds ) ) { + $this->recurrence = $this->interval_in_seconds; + unset( $this->interval_in_seconds ); + } + parent::__wakeup(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php new file mode 100644 index 0000000000..0ca9f7ca6f --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php @@ -0,0 +1,28 @@ +<?php + +/** + * Class ActionScheduler_NullSchedule + */ +class ActionScheduler_NullSchedule extends ActionScheduler_SimpleSchedule { + + /** + * Make the $date param optional and default to null. + * + * @param null $date The date & time to run the action. + */ + public function __construct( DateTime $date = null ) { + $this->scheduled_date = null; + } + + /** + * This schedule has no scheduled DateTime, so we need to override the parent __sleep() + * @return array + */ + public function __sleep() { + return array(); + } + + public function __wakeup() { + $this->scheduled_date = null; + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_Schedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_Schedule.php new file mode 100644 index 0000000000..d61a9f7c92 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_Schedule.php @@ -0,0 +1,18 @@ +<?php + +/** + * Class ActionScheduler_Schedule + */ +interface ActionScheduler_Schedule { + /** + * @param DateTime $after + * @return DateTime|null + */ + public function next( DateTime $after = NULL ); + + /** + * @return bool + */ + public function is_recurring(); +} + \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_SimpleSchedule.php b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_SimpleSchedule.php new file mode 100644 index 0000000000..454174c2ae --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schedules/ActionScheduler_SimpleSchedule.php @@ -0,0 +1,71 @@ +<?php + +/** + * Class ActionScheduler_SimpleSchedule + */ +class ActionScheduler_SimpleSchedule extends ActionScheduler_Abstract_Schedule { + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $timestamp = NULL; + + /** + * @param DateTime $after + * + * @return DateTime|null + */ + public function calculate_next( DateTime $after ) { + return null; + } + + /** + * @return bool + */ + public function is_recurring() { + return false; + } + + /** + * Serialize schedule with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * scheduled date for single actions always being seen as "now" if downgrading to + * Action Scheduler < 3.0.0, we need to also store the data with the old property names + * so if it's unserialized in AS < 3.0, the schedule doesn't end up with a null recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->timestamp = $this->scheduled_timestamp; + + return array_merge( $sleep_params, array( + 'timestamp', + ) ); + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To maintain backward + * compatibility with schedules serialized and stored prior to 3.0, we need to correctly + * map the old property names with matching visibility. + */ + public function __wakeup() { + + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->timestamp ) ) { + $this->scheduled_timestamp = $this->timestamp; + unset( $this->timestamp ); + } + parent::__wakeup(); + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php b/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php new file mode 100644 index 0000000000..af4aa5c571 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php @@ -0,0 +1,90 @@ +<?php + +/** + * Class ActionScheduler_LoggerSchema + * + * @codeCoverageIgnore + * + * Creates a custom table for storing action logs + */ +class ActionScheduler_LoggerSchema extends ActionScheduler_Abstract_Schema { + const LOG_TABLE = 'actionscheduler_logs'; + + /** + * @var int Increment this value to trigger a schema update. + */ + protected $schema_version = 3; + + public function __construct() { + $this->tables = [ + self::LOG_TABLE, + ]; + } + + /** + * Performs additional setup work required to support this schema. + */ + public function init() { + add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_3_0' ), 10, 2 ); + } + + protected function get_table_definition( $table ) { + global $wpdb; + $table_name = $wpdb->$table; + $charset_collate = $wpdb->get_charset_collate(); + switch ( $table ) { + + case self::LOG_TABLE: + + $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; + return "CREATE TABLE {$table_name} ( + log_id bigint(20) unsigned NOT NULL auto_increment, + action_id bigint(20) unsigned NOT NULL, + message text NOT NULL, + log_date_gmt datetime NULL default '${default_date}', + log_date_local datetime NULL default '${default_date}', + PRIMARY KEY (log_id), + KEY action_id (action_id), + KEY log_date_gmt (log_date_gmt) + ) $charset_collate"; + + default: + return ''; + } + } + + /** + * Update the logs table schema, allowing datetime fields to be NULL. + * + * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL + * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. + * + * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however + * that method relies on dbDelta() and this change is not possible when using that function. + * + * @param string $table Name of table being updated. + * @param string $db_version The existing schema version of the table. + */ + public function update_schema_3_0( $table, $db_version ) { + global $wpdb; + + if ( 'actionscheduler_logs' !== $table || version_compare( $db_version, '3', '>=' ) ) { + return; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $table_name = $wpdb->prefix . 'actionscheduler_logs'; + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; + + if ( ! empty( $table_list ) ) { + $query = " + ALTER TABLE ${table_name} + MODIFY COLUMN log_date_gmt datetime NULL default '${default_date}', + MODIFY COLUMN log_date_local datetime NULL default '${default_date}' + "; + $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } +} diff --git a/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php b/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php new file mode 100644 index 0000000000..2506f01806 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php @@ -0,0 +1,129 @@ +<?php + +/** + * Class ActionScheduler_StoreSchema + * + * @codeCoverageIgnore + * + * Creates custom tables for storing scheduled actions + */ +class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema { + const ACTIONS_TABLE = 'actionscheduler_actions'; + const CLAIMS_TABLE = 'actionscheduler_claims'; + const GROUPS_TABLE = 'actionscheduler_groups'; + const DEFAULT_DATE = '0000-00-00 00:00:00'; + + /** + * @var int Increment this value to trigger a schema update. + */ + protected $schema_version = 6; + + public function __construct() { + $this->tables = [ + self::ACTIONS_TABLE, + self::CLAIMS_TABLE, + self::GROUPS_TABLE, + ]; + } + + /** + * Performs additional setup work required to support this schema. + */ + public function init() { + add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_5_0' ), 10, 2 ); + } + + protected function get_table_definition( $table ) { + global $wpdb; + $table_name = $wpdb->$table; + $charset_collate = $wpdb->get_charset_collate(); + $max_index_length = 191; // @see wp_get_db_schema() + $default_date = self::DEFAULT_DATE; + switch ( $table ) { + + case self::ACTIONS_TABLE: + + return "CREATE TABLE {$table_name} ( + action_id bigint(20) unsigned NOT NULL auto_increment, + hook varchar(191) NOT NULL, + status varchar(20) NOT NULL, + scheduled_date_gmt datetime NULL default '${default_date}', + scheduled_date_local datetime NULL default '${default_date}', + args varchar($max_index_length), + schedule longtext, + group_id bigint(20) unsigned NOT NULL default '0', + attempts int(11) NOT NULL default '0', + last_attempt_gmt datetime NULL default '${default_date}', + last_attempt_local datetime NULL default '${default_date}', + claim_id bigint(20) unsigned NOT NULL default '0', + extended_args varchar(8000) DEFAULT NULL, + PRIMARY KEY (action_id), + KEY hook (hook($max_index_length)), + KEY status (status), + KEY scheduled_date_gmt (scheduled_date_gmt), + KEY args (args($max_index_length)), + KEY group_id (group_id), + KEY last_attempt_gmt (last_attempt_gmt), + KEY `claim_id_status_scheduled_date_gmt` (`claim_id`, `status`, `scheduled_date_gmt`) + ) $charset_collate"; + + case self::CLAIMS_TABLE: + + return "CREATE TABLE {$table_name} ( + claim_id bigint(20) unsigned NOT NULL auto_increment, + date_created_gmt datetime NULL default '${default_date}', + PRIMARY KEY (claim_id), + KEY date_created_gmt (date_created_gmt) + ) $charset_collate"; + + case self::GROUPS_TABLE: + + return "CREATE TABLE {$table_name} ( + group_id bigint(20) unsigned NOT NULL auto_increment, + slug varchar(255) NOT NULL, + PRIMARY KEY (group_id), + KEY slug (slug($max_index_length)) + ) $charset_collate"; + + default: + return ''; + } + } + + /** + * Update the actions table schema, allowing datetime fields to be NULL. + * + * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL + * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. + * + * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however + * that method relies on dbDelta() and this change is not possible when using that function. + * + * @param string $table Name of table being updated. + * @param string $db_version The existing schema version of the table. + */ + public function update_schema_5_0( $table, $db_version ) { + global $wpdb; + + if ( 'actionscheduler_actions' !== $table || version_compare( $db_version, '5', '>=' ) ) { + return; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $table_name = $wpdb->prefix . 'actionscheduler_actions'; + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $default_date = self::DEFAULT_DATE; + + if ( ! empty( $table_list ) ) { + $query = " + ALTER TABLE ${table_name} + MODIFY COLUMN scheduled_date_gmt datetime NULL default '${default_date}', + MODIFY COLUMN scheduled_date_local datetime NULL default '${default_date}', + MODIFY COLUMN last_attempt_gmt datetime NULL default '${default_date}', + MODIFY COLUMN last_attempt_local datetime NULL default '${default_date}' + "; + $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } +} diff --git a/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php new file mode 100644 index 0000000000..dac17aa42e --- /dev/null +++ b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php @@ -0,0 +1,27 @@ +<?php + +/** + * Abstract class with common Queue Cleaner functionality. + */ +abstract class ActionScheduler_Abstract_QueueRunner_Deprecated { + + /** + * Get the maximum number of seconds a batch can run for. + * + * @deprecated 2.1.1 + * @return int The number of seconds. + */ + protected function get_maximum_execution_time() { + _deprecated_function( __METHOD__, '2.1.1', 'ActionScheduler_Abstract_QueueRunner::get_time_limit()' ); + + $maximum_execution_time = 30; + + // Apply deprecated filter + if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { + _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); + $maximum_execution_time = apply_filters( 'action_scheduler_maximum_execution_time', $maximum_execution_time ); + } + + return absint( $maximum_execution_time ); + } +} diff --git a/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_AdminView_Deprecated.php b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_AdminView_Deprecated.php new file mode 100644 index 0000000000..69b46d7bd4 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_AdminView_Deprecated.php @@ -0,0 +1,147 @@ +<?php + +/** + * Class ActionScheduler_AdminView_Deprecated + * + * Store deprecated public functions previously found in the ActionScheduler_AdminView class. + * Keeps them out of the way of the main class. + * + * @codeCoverageIgnore + */ +class ActionScheduler_AdminView_Deprecated { + + public function action_scheduler_post_type_args( $args ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $args; + } + + /** + * Customise the post status related views displayed on the Scheduled Actions administration screen. + * + * @param array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen. + * @return array $views An associative array of views and view labels which can be used to filter the 'scheduled-action' posts displayed on the Scheduled Actions administration screen. + */ + public function list_table_views( $views ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $views; + } + + /** + * Do not include the "Edit" action for the Scheduled Actions administration screen. + * + * Hooked to the 'bulk_actions-edit-action-scheduler' filter. + * + * @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. + * @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. + */ + public function bulk_actions( $actions ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $actions; + } + + /** + * Completely customer the columns displayed on the Scheduled Actions administration screen. + * + * Because we can't filter the content of the default title and date columns, we need to recreate our own + * custom columns for displaying those post fields. For the column content, @see self::list_table_column_content(). + * + * @param array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen. + * @return array $columns An associative array of columns that are use for the table on the Scheduled Actions administration screen. + */ + public function list_table_columns( $columns ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $columns; + } + + /** + * Make our custom title & date columns use defaulting title & date sorting. + * + * @param array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen. + * @return array $columns An associative array of columns that can be used to sort the table on the Scheduled Actions administration screen. + */ + public static function list_table_sortable_columns( $columns ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $columns; + } + + /** + * Print the content for our custom columns. + * + * @param string $column_name The key for the column for which we should output our content. + * @param int $post_id The ID of the 'scheduled-action' post for which this row relates. + */ + public static function list_table_column_content( $column_name, $post_id ) { + _deprecated_function( __METHOD__, '2.0.0' ); + } + + /** + * Hide the inline "Edit" action for all 'scheduled-action' posts. + * + * Hooked to the 'post_row_actions' filter. + * + * @param array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. + * @return array $actions An associative array of actions which can be performed on the 'scheduled-action' post type. + */ + public static function row_actions( $actions, $post ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $actions; + } + + /** + * Run an action when triggered from the Action Scheduler administration screen. + * + * @codeCoverageIgnore + */ + public static function maybe_execute_action() { + _deprecated_function( __METHOD__, '2.0.0' ); + } + + /** + * Convert an interval of seconds into a two part human friendly string. + * + * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning + * even if an action is 1 day and 11 hours away, it will display "1 day". This funciton goes one step + * further to display two degrees of accuracy. + * + * Based on Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ + * + * @param int $interval A interval in seconds. + * @return string A human friendly string representation of the interval. + */ + public static function admin_notices() { + _deprecated_function( __METHOD__, '2.0.0' ); + } + + /** + * Filter search queries to allow searching by Claim ID (i.e. post_password). + * + * @param string $orderby MySQL orderby string. + * @param WP_Query $query Instance of a WP_Query object + * @return string MySQL orderby string. + */ + public function custom_orderby( $orderby, $query ){ + _deprecated_function( __METHOD__, '2.0.0' ); + } + + /** + * Filter search queries to allow searching by Claim ID (i.e. post_password). + * + * @param string $search MySQL search string. + * @param WP_Query $query Instance of a WP_Query object + * @return string MySQL search string. + */ + public function search_post_password( $search, $query ) { + _deprecated_function( __METHOD__, '2.0.0' ); + } + + /** + * Change messages when a scheduled action is updated. + * + * @param array $messages + * @return array + */ + public function post_updated_messages( $messages ) { + _deprecated_function( __METHOD__, '2.0.0' ); + return $messages; + } +} \ No newline at end of file diff --git a/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Schedule_Deprecated.php b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Schedule_Deprecated.php new file mode 100644 index 0000000000..496d67b840 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Schedule_Deprecated.php @@ -0,0 +1,29 @@ +<?php + +/** + * Class ActionScheduler_Abstract_Schedule + */ +abstract class ActionScheduler_Schedule_Deprecated implements ActionScheduler_Schedule { + + /** + * Get the date & time this schedule was created to run, or calculate when it should be run + * after a given date & time. + * + * @param DateTime $after DateTime to calculate against. + * + * @return DateTime|null + */ + public function next( DateTime $after = null ) { + if ( empty( $after ) ) { + $return_value = $this->get_date(); + $replacement_method = 'get_date()'; + } else { + $return_value = $this->get_next( $after ); + $replacement_method = 'get_next( $after )'; + } + + _deprecated_function( __METHOD__, '3.0.0', __CLASS__ . '::' . $replacement_method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + return $return_value; + } +} diff --git a/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php new file mode 100644 index 0000000000..002dc75b41 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php @@ -0,0 +1,49 @@ +<?php + +/** + * Class ActionScheduler_Store_Deprecated + * @codeCoverageIgnore + */ +abstract class ActionScheduler_Store_Deprecated { + + /** + * Mark an action that failed to fetch correctly as failed. + * + * @since 2.2.6 + * + * @param int $action_id The ID of the action. + */ + public function mark_failed_fetch_action( $action_id ) { + _deprecated_function( __METHOD__, '3.0.0', 'ActionScheduler_Store::mark_failure()' ); + self::$store->mark_failure( $action_id ); + } + + /** + * Add base hooks + * + * @since 2.2.6 + */ + protected static function hook() { + _deprecated_function( __METHOD__, '3.0.0' ); + } + + /** + * Remove base hooks + * + * @since 2.2.6 + */ + protected static function unhook() { + _deprecated_function( __METHOD__, '3.0.0' ); + } + + /** + * Get the site's local time. + * + * @deprecated 2.1.0 + * @return DateTimeZone + */ + protected function get_local_timezone() { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); + return ActionScheduler_TimezoneHelper::get_local_timezone(); + } +} diff --git a/vendor/woocommerce/action-scheduler/deprecated/functions.php b/vendor/woocommerce/action-scheduler/deprecated/functions.php new file mode 100644 index 0000000000..f782c4b7f0 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/deprecated/functions.php @@ -0,0 +1,126 @@ +<?php + +/** + * Deprecated API functions for scheduling actions + * + * Functions with the wc prefix were deprecated to avoid confusion with + * Action Scheduler being included in WooCommerce core, and it providing + * a different set of APIs for working with the action queue. + */ + +/** + * Schedule an action to run one time + * + * @param int $timestamp When the job will run + * @param string $hook The hook to trigger + * @param array $args Arguments to pass when the hook triggers + * @param string $group The group to assign this job to + * + * @return string The job ID + */ +function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_single_action()' ); + return as_schedule_single_action( $timestamp, $hook, $args, $group ); +} + +/** + * Schedule a recurring action + * + * @param int $timestamp When the first instance of the job will run + * @param int $interval_in_seconds How long to wait between runs + * @param string $hook The hook to trigger + * @param array $args Arguments to pass when the hook triggers + * @param string $group The group to assign this job to + * + * @deprecated 2.1.0 + * + * @return string The job ID + */ +function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_recurring_action()' ); + return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); +} + +/** + * Schedule an action that recurs on a cron-like schedule. + * + * @param int $timestamp The schedule will start on or after this time + * @param string $schedule A cron-link schedule string + * @see http://en.wikipedia.org/wiki/Cron + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * | | | | | | + * | | | | | + year [optional] + * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + * | | | +---------- month (1 - 12) + * | | +--------------- day of month (1 - 31) + * | +-------------------- hour (0 - 23) + * +------------------------- min (0 - 59) + * @param string $hook The hook to trigger + * @param array $args Arguments to pass when the hook triggers + * @param string $group The group to assign this job to + * + * @deprecated 2.1.0 + * + * @return string The job ID + */ +function wc_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_cron_action()' ); + return as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group ); +} + +/** + * Cancel the next occurrence of a job. + * + * @param string $hook The hook that the job will trigger + * @param array $args Args that would have been passed to the job + * @param string $group + * + * @deprecated 2.1.0 + */ +function wc_unschedule_action( $hook, $args = array(), $group = '' ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_unschedule_action()' ); + as_unschedule_action( $hook, $args, $group ); +} + +/** + * @param string $hook + * @param array $args + * @param string $group + * + * @deprecated 2.1.0 + * + * @return int|bool The timestamp for the next occurrence, or false if nothing was found + */ +function wc_next_scheduled_action( $hook, $args = NULL, $group = '' ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_next_scheduled_action()' ); + return as_next_scheduled_action( $hook, $args, $group ); +} + +/** + * Find scheduled actions + * + * @param array $args Possible arguments, with their default values: + * 'hook' => '' - the name of the action that will be triggered + * 'args' => NULL - the args array that will be passed with the action + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' + * 'order' => 'ASC' + * @param string $return_format OBJECT, ARRAY_A, or ids + * + * @deprecated 2.1.0 + * + * @return array + */ +function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); + return as_get_scheduled_actions( $args, $return_format ); +} diff --git a/vendor/woocommerce/action-scheduler/functions.php b/vendor/woocommerce/action-scheduler/functions.php new file mode 100644 index 0000000000..5f0554674e --- /dev/null +++ b/vendor/woocommerce/action-scheduler/functions.php @@ -0,0 +1,319 @@ +<?php + +/** + * General API functions for scheduling actions + */ + +/** + * Enqueue an action to run one time, as soon as possible + * + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return int The action ID. + */ +function as_enqueue_async_action( $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->async( $hook, $args, $group ); +} + +/** + * Schedule an action to run one time + * + * @param int $timestamp When the job will run. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->single( $hook, $args, $timestamp, $group ); +} + +/** + * Schedule a recurring action + * + * @param int $timestamp When the first instance of the job will run. + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->recurring( $hook, $args, $timestamp, $interval_in_seconds, $group ); +} + +/** + * Schedule an action that recurs on a cron-like schedule. + * + * @param int $base_timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param string $schedule A cron-link schedule string + * @see http://en.wikipedia.org/wiki/Cron + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * | | | | | | + * | | | | | + year [optional] + * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + * | | | +---------- month (1 - 12) + * | | +--------------- day of month (1 - 31) + * | +-------------------- hour (0 - 23) + * +------------------------- min (0 - 59) + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->cron( $hook, $args, $timestamp, $schedule, $group ); +} + +/** + * Cancel the next occurrence of a scheduled action. + * + * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent + * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in + * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled + * only after the former action is run. If the next instance is never run, because it's unscheduled by this function, + * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled + * by this method also. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to. + * + * @return string|null The scheduled action ID if a scheduled action was found, or null if no matching action found. + */ +function as_unschedule_action( $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + $params = array( + 'hook' => $hook, + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'orderby' => 'date', + 'order' => 'ASC', + 'group' => $group, + ); + if ( is_array( $args ) ) { + $params['args'] = $args; + } + + $action_id = ActionScheduler::store()->query_action( $params ); + if ( $action_id ) { + ActionScheduler::store()->cancel_action( $action_id ); + } + + return $action_id; +} + +/** + * Cancel all occurrences of a scheduled action. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to. + */ +function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return; + } + if ( empty( $args ) ) { + if ( ! empty( $hook ) && empty( $group ) ) { + ActionScheduler_Store::instance()->cancel_actions_by_hook( $hook ); + return; + } + if ( ! empty( $group ) && empty( $hook ) ) { + ActionScheduler_Store::instance()->cancel_actions_by_group( $group ); + return; + } + } + do { + $unscheduled_action = as_unschedule_action( $hook, $args, $group ); + } while ( ! empty( $unscheduled_action ) ); +} + +/** + * Check if there is an existing action in the queue with a given hook, args and group combination. + * + * An action in the queue could be pending, in-progress or async. If the is pending for a time in + * future, its scheduled date will be returned as a timestamp. If it is currently being run, or an + * async action sitting in the queue waiting to be processed, in which case boolean true will be + * returned. Or there may be no async, in-progress or pending action for this hook, in which case, + * boolean false will be the return value. + * + * @param string $hook + * @param array $args + * @param string $group + * + * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. + */ +function as_next_scheduled_action( $hook, $args = null, $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return false; + } + + $params = array( + 'hook' => $hook, + 'orderby' => 'date', + 'order' => 'ASC', + 'group' => $group, + ); + + if ( is_array( $args ) ) { + $params['args'] = $args; + } + + $params['status'] = ActionScheduler_Store::STATUS_RUNNING; + $action_id = ActionScheduler::store()->query_action( $params ); + if ( $action_id ) { + return true; + } + + $params['status'] = ActionScheduler_Store::STATUS_PENDING; + $action_id = ActionScheduler::store()->query_action( $params ); + if ( null === $action_id ) { + return false; + } + + $action = ActionScheduler::store()->fetch_action( $action_id ); + $scheduled_date = $action->get_schedule()->get_date(); + if ( $scheduled_date ) { + return (int) $scheduled_date->format( 'U' ); + } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule + return true; + } + + return false; +} + +/** + * Check if there is a scheduled action in the queue but more efficiently than as_next_scheduled_action(). + * + * It's recommended to use this function when you need to know whether a specific action is currently scheduled + * (pending or in-progress). + * + * @since x.x.x + * + * @param string $hook The hook of the action. + * @param array $args Args that have been passed to the action. Null will matches any args. + * @param string $group The group the job is assigned to. + * + * @return bool True if a matching action is pending or in-progress, false otherwise. + */ +function as_has_scheduled_action( $hook, $args = null, $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return false; + } + + $query_args = array( + 'hook' => $hook, + 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), + 'group' => $group, + 'orderby' => 'none', + ); + + if ( null !== $args ) { + $query_args['args'] = $args; + } + + $action_id = ActionScheduler::store()->query_action( $query_args ); + + return $action_id !== null; +} + +/** + * Find scheduled actions + * + * @param array $args Possible arguments, with their default values: + * 'hook' => '' - the name of the action that will be triggered + * 'args' => NULL - the args array that will be passed with the action + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none' + * 'order' => 'ASC' + * + * @param string $return_format OBJECT, ARRAY_A, or ids. + * + * @return array + */ +function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return array(); + } + $store = ActionScheduler::store(); + foreach ( array('date', 'modified') as $key ) { + if ( isset($args[$key]) ) { + $args[$key] = as_get_datetime_object($args[$key]); + } + } + $ids = $store->query_actions( $args ); + + if ( $return_format == 'ids' || $return_format == 'int' ) { + return $ids; + } + + $actions = array(); + foreach ( $ids as $action_id ) { + $actions[$action_id] = $store->fetch_action( $action_id ); + } + + if ( $return_format == ARRAY_A ) { + foreach ( $actions as $action_id => $action_object ) { + $actions[$action_id] = get_object_vars($action_object); + } + } + + return $actions; +} + +/** + * Helper function to create an instance of DateTime based on a given + * string and timezone. By default, will return the current date/time + * in the UTC timezone. + * + * Needed because new DateTime() called without an explicit timezone + * will create a date/time in PHP's timezone, but we need to have + * assurance that a date/time uses the right timezone (which we almost + * always want to be UTC), which means we need to always include the + * timezone when instantiating datetimes rather than leaving it up to + * the PHP default. + * + * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. + * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php. + * + * @return ActionScheduler_DateTime + */ +function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) { + if ( is_object( $date_string ) && $date_string instanceof DateTime ) { + $date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) ); + } elseif ( is_numeric( $date_string ) ) { + $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) ); + } else { + $date = new ActionScheduler_DateTime( $date_string, new DateTimeZone( $timezone ) ); + } + return $date; +} diff --git a/vendor/woocommerce/action-scheduler/lib/WP_Async_Request.php b/vendor/woocommerce/action-scheduler/lib/WP_Async_Request.php new file mode 100644 index 0000000000..d7dea1c21c --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/WP_Async_Request.php @@ -0,0 +1,170 @@ +<?php +/** + * WP Async Request + * + * @package WP-Background-Processing + */ +/* +Library URI: https://github.com/deliciousbrains/wp-background-processing/blob/fbbc56f2480910d7959972ec9ec0819a13c6150a/classes/wp-async-request.php +Author: Delicious Brains Inc. +Author URI: https://deliciousbrains.com/ +License: GNU General Public License v2.0 +License URI: https://github.com/deliciousbrains/wp-background-processing/commit/126d7945dd3d39f39cb6488ca08fe1fb66cb351a +*/ + +if ( ! class_exists( 'WP_Async_Request' ) ) { + + /** + * Abstract WP_Async_Request class. + * + * @abstract + */ + abstract class WP_Async_Request { + + /** + * Prefix + * + * (default value: 'wp') + * + * @var string + * @access protected + */ + protected $prefix = 'wp'; + + /** + * Action + * + * (default value: 'async_request') + * + * @var string + * @access protected + */ + protected $action = 'async_request'; + + /** + * Identifier + * + * @var mixed + * @access protected + */ + protected $identifier; + + /** + * Data + * + * (default value: array()) + * + * @var array + * @access protected + */ + protected $data = array(); + + /** + * Initiate new async request + */ + public function __construct() { + $this->identifier = $this->prefix . '_' . $this->action; + + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); + } + + /** + * Set data used during the request + * + * @param array $data Data. + * + * @return $this + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } + + /** + * Dispatch the async request + * + * @return array|WP_Error + */ + public function dispatch() { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * Get query args + * + * @return array + */ + protected function get_query_args() { + if ( property_exists( $this, 'query_args' ) ) { + return $this->query_args; + } + + return array( + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ); + } + + /** + * Get query URL + * + * @return string + */ + protected function get_query_url() { + if ( property_exists( $this, 'query_url' ) ) { + return $this->query_url; + } + + return admin_url( 'admin-ajax.php' ); + } + + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; + } + + return array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + } + + /** + * Maybe handle + * + * Check for correct nonce and pass to handler. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Handle + * + * Override this method to perform any actions required + * during the async request. + */ + abstract protected function handle(); + + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression.php new file mode 100644 index 0000000000..7f33c378f9 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression.php @@ -0,0 +1,318 @@ +<?php + +/** + * CRON expression parser that can determine whether or not a CRON expression is + * due to run, the next run date and previous run date of a CRON expression. + * The determinations made by this class are accurate if checked run once per + * minute (seconds are dropped from date time comparisons). + * + * Schedule parts must map to: + * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week + * [1-7|MON-SUN], and an optional year. + * + * @author Michael Dowling <mtdowling@gmail.com> + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression +{ + const MINUTE = 0; + const HOUR = 1; + const DAY = 2; + const MONTH = 3; + const WEEKDAY = 4; + const YEAR = 5; + + /** + * @var array CRON expression parts + */ + private $cronParts; + + /** + * @var CronExpression_FieldFactory CRON field factory + */ + private $fieldFactory; + + /** + * @var array Order in which to test of cron parts + */ + private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); + + /** + * Factory method to create a new CronExpression. + * + * @param string $expression The CRON expression to create. There are + * several special predefined values which can be used to substitute the + * CRON expression: + * + * @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 * + * @monthly - Run once a month, midnight, first of month - 0 0 1 * * + * @weekly - Run once a week, midnight on Sun - 0 0 * * 0 + * @daily - Run once a day, midnight - 0 0 * * * + * @hourly - Run once an hour, first minute - 0 * * * * + * +*@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use + * + * @return CronExpression + */ + public static function factory($expression, CronExpression_FieldFactory $fieldFactory = null) + { + $mappings = array( + '@yearly' => '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@hourly' => '0 * * * *' + ); + + if (isset($mappings[$expression])) { + $expression = $mappings[$expression]; + } + + return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory()); + } + + /** + * Parse a CRON expression + * + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields + */ + public function __construct($expression, CronExpression_FieldFactory $fieldFactory) + { + $this->fieldFactory = $fieldFactory; + $this->setExpression($expression); + } + + /** + * Set or change the CRON expression + * + * @param string $value CRON expression (e.g. 8 * * * *) + * + * @return CronExpression + * @throws InvalidArgumentException if not a valid CRON expression + */ + public function setExpression($value) + { + $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + if (count($this->cronParts) < 5) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } + + foreach ($this->cronParts as $position => $part) { + $this->setPart($position, $part); + } + + return $this; + } + + /** + * Set part of the CRON expression + * + * @param int $position The position of the CRON expression to set + * @param string $value The value to set + * + * @return CronExpression + * @throws InvalidArgumentException if the value is not valid for the part + */ + public function setPart($position, $value) + { + if (!$this->fieldFactory->getField($position)->validate($value)) { + throw new InvalidArgumentException( + 'Invalid CRON field value ' . $value . ' as position ' . $position + ); + } + + $this->cronParts[$position] = $value; + + return $this; + } + + /** + * Get a next run date relative to the current date or a specific date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning a + * matching next run date. 0, the default, will return the current + * date and time if the next run date falls on the current date and + * time. Setting this value to 1 will skip the first match and go to + * the second match. Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + */ + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) + { + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate); + } + + /** + * Get a previous run date relative to the current date or a specific date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + * @see CronExpression::getNextRunDate + */ + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) + { + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate); + } + + /** + * Get multiple run dates starting at the current date or a specific date + * + * @param int $total Set the total number of dates to calculate + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param bool $invert (optional) Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return array Returns an array of run dates + */ + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false) + { + $matches = array(); + for ($i = 0; $i < max(0, $total); $i++) { + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate); + } + + return $matches; + } + + /** + * Get all or part of the CRON expression + * + * @param string $part (optional) Specify the part to retrieve or NULL to + * get the full cron schedule string. + * + * @return string|null Returns the CRON expression, a part of the + * CRON expression, or NULL if the part was specified but not found + */ + public function getExpression($part = null) + { + if (null === $part) { + return implode(' ', $this->cronParts); + } elseif (array_key_exists($part, $this->cronParts)) { + return $this->cronParts[$part]; + } + + return null; + } + + /** + * Helper method to output the full expression. + * + * @return string Full CRON expression + */ + public function __toString() + { + return $this->getExpression(); + } + + /** + * Determine if the cron is due to run based on the current date or a + * specific date. This method assumes that the current number of + * seconds are irrelevant, and should be called once per minute. + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * + * @return bool Returns TRUE if the cron is due to run or FALSE if not + */ + public function isDue($currentTime = 'now') + { + if ('now' === $currentTime) { + $currentDate = date('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } elseif ($currentTime instanceof DateTime) { + $currentDate = $currentTime->format('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } else { + $currentTime = new DateTime($currentTime); + $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); + $currentDate = $currentTime->format('Y-m-d H:i'); + $currentTime = (int)($currentTime->getTimestamp()); + } + + return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; + } + + /** + * Get the next or previous run date of the expression relative to a date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning + * @param bool $invert (optional) Set to TRUE to go backwards in time + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + */ + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false) + { + if ($currentTime instanceof DateTime) { + $currentDate = $currentTime; + } else { + $currentDate = new DateTime($currentTime ? $currentTime : 'now'); + $currentDate->setTimezone(new DateTimeZone(date_default_timezone_get())); + } + + $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); + $nextRun = clone $currentDate; + $nth = (int) $nth; + + // Set a hard limit to bail on an impossible date + for ($i = 0; $i < 1000; $i++) { + + foreach (self::$order as $position) { + $part = $this->getExpression($position); + if (null === $part) { + continue; + } + + $satisfied = false; + // Get the field object used to validate this part + $field = $this->fieldFactory->getField($position); + // Check if this is singular or a list + if (strpos($part, ',') === false) { + $satisfied = $field->isSatisfiedBy($nextRun, $part); + } else { + foreach (array_map('trim', explode(',', $part)) as $listPart) { + if ($field->isSatisfiedBy($nextRun, $listPart)) { + $satisfied = true; + break; + } + } + } + + // If the field is not satisfied, then start over + if (!$satisfied) { + $field->increment($nextRun, $invert); + continue 2; + } + } + + // Skip this match if needed + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { + $this->fieldFactory->getField(0)->increment($nextRun, $invert); + continue; + } + + return $nextRun; + } + + // @codeCoverageIgnoreStart + throw new RuntimeException('Impossible CRON expression'); + // @codeCoverageIgnoreEnd + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php new file mode 100644 index 0000000000..f8d5c00ae7 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php @@ -0,0 +1,100 @@ +<?php + +/** + * Abstract CRON expression field + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +abstract class CronExpression_AbstractField implements CronExpression_FieldInterface +{ + /** + * Check to see if a field is satisfied by a value + * + * @param string $dateValue Date value to check + * @param string $value Value to test + * + * @return bool + */ + public function isSatisfied($dateValue, $value) + { + if ($this->isIncrementsOfRanges($value)) { + return $this->isInIncrementsOfRanges($dateValue, $value); + } elseif ($this->isRange($value)) { + return $this->isInRange($dateValue, $value); + } + + return $value == '*' || $dateValue == $value; + } + + /** + * Check if a value is a range + * + * @param string $value Value to test + * + * @return bool + */ + public function isRange($value) + { + return strpos($value, '-') !== false; + } + + /** + * Check if a value is an increments of ranges + * + * @param string $value Value to test + * + * @return bool + */ + public function isIncrementsOfRanges($value) + { + return strpos($value, '/') !== false; + } + + /** + * Test if a value is within a range + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInRange($dateValue, $value) + { + $parts = array_map('trim', explode('-', $value, 2)); + + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; + } + + /** + * Test if a value is within an increments of ranges (offset[-to]/step size) + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInIncrementsOfRanges($dateValue, $value) + { + $parts = array_map('trim', explode('/', $value, 2)); + $stepSize = isset($parts[1]) ? $parts[1] : 0; + if ($parts[0] == '*' || $parts[0] === '0') { + return (int) $dateValue % $stepSize == 0; + } + + $range = explode('-', $parts[0], 2); + $offset = $range[0]; + $to = isset($range[1]) ? $range[1] : $dateValue; + // Ensure that the date value is within the range + if ($dateValue < $offset || $dateValue > $to) { + return false; + } + + for ($i = $offset; $i <= $to; $i+= $stepSize) { + if ($i == $dateValue) { + return true; + } + } + + return false; + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php new file mode 100644 index 0000000000..40c1d6c6e7 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php @@ -0,0 +1,110 @@ +<?php + +/** + * Day of month field. Allows: * , / - ? L W + * + * 'L' stands for "last" and specifies the last day of the month. + * + * The 'W' character is used to specify the weekday (Monday-Friday) nearest the + * given day. As an example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of the + * month". So if the 15th is a Saturday, the trigger will fire on Friday the + * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If + * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you + * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the + * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary + * of a month's days. The 'W' character can only be specified when the + * day-of-month is a single day, not a range or list of days. + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_DayOfMonthField extends CronExpression_AbstractField +{ + /** + * Get the nearest day of the week for a given day in a month + * + * @param int $currentYear Current year + * @param int $currentMonth Current month + * @param int $targetDay Target day of the month + * + * @return DateTime Returns the nearest date + */ + private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) + { + $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); + $target = new DateTime("$currentYear-$currentMonth-$tday"); + $currentWeekday = (int) $target->format('N'); + + if ($currentWeekday < 6) { + return $target; + } + + $lastDayOfMonth = $target->format('t'); + + foreach (array(-1, 1, -2, 2) as $i) { + $adjusted = $targetDay + $i; + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { + $target->setDate($currentYear, $currentMonth, $adjusted); + if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { + return $target; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + // ? states that the field value is to be skipped + if ($value == '?') { + return true; + } + + $fieldValue = $date->format('d'); + + // Check to see if this is the last day of the month + if ($value == 'L') { + return $fieldValue == $date->format('t'); + } + + // Check to see if this is the nearest weekday to a particular value + if (strpos($value, 'W')) { + // Parse the target day + $targetDay = substr($value, 0, strpos($value, 'W')); + // Find out if the current day is the nearest day of the week + return $date->format('j') == self::getNearestWeekday( + $date->format('Y'), + $date->format('m'), + $targetDay + )->format('j'); + } + + return $this->isSatisfied($date->format('d'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('previous day'); + $date->setTime(23, 59); + } else { + $date->modify('next day'); + $date->setTime(0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php new file mode 100644 index 0000000000..e9f68a7cd6 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php @@ -0,0 +1,124 @@ +<?php + +/** + * Day of week field. Allows: * / , - ? L # + * + * Days of the week can be represented as a number 0-7 (0|7 = Sunday) + * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT. + * + * 'L' stands for "last". It allows you to specify constructs such as + * "the last Friday" of a given month. + * + * '#' is allowed for the day-of-week field, and must be followed by a + * number between one and five. It allows you to specify constructs such as + * "the second Friday" of a given month. + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_DayOfWeekField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + if ($value == '?') { + return true; + } + + // Convert text day of the week values to integers + $value = str_ireplace( + array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'), + range(0, 6), + $value + ); + + $currentYear = $date->format('Y'); + $currentMonth = $date->format('m'); + $lastDayOfMonth = $date->format('t'); + + // Find out if this is the last specific weekday of the month + if (strpos($value, 'L')) { + $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + while ($tdate->format('w') != $weekday) { + $tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth); + } + + return $date->format('j') == $lastDayOfMonth; + } + + // Handle # hash tokens + if (strpos($value, '#')) { + list($weekday, $nth) = explode('#', $value); + // Validate the hash fields + if ($weekday < 1 || $weekday > 5) { + throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given"); + } + if ($nth > 5) { + throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month'); + } + // The current weekday must match the targeted weekday to proceed + if ($date->format('N') != $weekday) { + return false; + } + + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, 1); + $dayCount = 0; + $currentDay = 1; + while ($currentDay < $lastDayOfMonth + 1) { + if ($tdate->format('N') == $weekday) { + if (++$dayCount >= $nth) { + break; + } + } + $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + } + + return $date->format('j') == $currentDay; + } + + // Handle day of the week values + if (strpos($value, '-')) { + $parts = explode('-', $value); + if ($parts[0] == '7') { + $parts[0] = '0'; + } elseif ($parts[1] == '0') { + $parts[1] = '7'; + } + $value = implode('-', $parts); + } + + // Test to see which Sunday to use -- 0 == 7 == Sunday + $format = in_array(7, str_split($value)) ? 'N' : 'w'; + $fieldValue = $date->format($format); + + return $this->isSatisfied($fieldValue, $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 day'); + $date->setTime(23, 59, 0); + } else { + $date->modify('+1 day'); + $date->setTime(0, 0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php new file mode 100644 index 0000000000..556ba1a3e3 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php @@ -0,0 +1,55 @@ +<?php + +/** + * CRON field factory implementing a flyweight factory + * + * @author Michael Dowling <mtdowling@gmail.com> + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression_FieldFactory +{ + /** + * @var array Cache of instantiated fields + */ + private $fields = array(); + + /** + * Get an instance of a field object for a cron expression position + * + * @param int $position CRON expression position value to retrieve + * + * @return CronExpression_FieldInterface + * @throws InvalidArgumentException if a position is not valid + */ + public function getField($position) + { + if (!isset($this->fields[$position])) { + switch ($position) { + case 0: + $this->fields[$position] = new CronExpression_MinutesField(); + break; + case 1: + $this->fields[$position] = new CronExpression_HoursField(); + break; + case 2: + $this->fields[$position] = new CronExpression_DayOfMonthField(); + break; + case 3: + $this->fields[$position] = new CronExpression_MonthField(); + break; + case 4: + $this->fields[$position] = new CronExpression_DayOfWeekField(); + break; + case 5: + $this->fields[$position] = new CronExpression_YearField(); + break; + default: + throw new InvalidArgumentException( + $position . ' is not a valid position' + ); + } + } + + return $this->fields[$position]; + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php new file mode 100644 index 0000000000..5d5109b70d --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php @@ -0,0 +1,39 @@ +<?php + +/** + * CRON field interface + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +interface CronExpression_FieldInterface +{ + /** + * Check if the respective value of a DateTime field satisfies a CRON exp + * + * @param DateTime $date DateTime object to check + * @param string $value CRON expression to test against + * + * @return bool Returns TRUE if satisfied, FALSE otherwise + */ + public function isSatisfiedBy(DateTime $date, $value); + + /** + * When a CRON expression is not satisfied, this method is used to increment + * or decrement a DateTime object by the unit of the cron field + * + * @param DateTime $date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement + * + * @return CronExpression_FieldInterface + */ + public function increment(DateTime $date, $invert = false); + + /** + * Validates a CRON expression for a given field + * + * @param string $value CRON expression value to validate + * + * @return bool Returns TRUE if valid, FALSE otherwise + */ + public function validate($value); +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_HoursField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_HoursField.php new file mode 100644 index 0000000000..088ca73c71 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_HoursField.php @@ -0,0 +1,47 @@ +<?php + +/** + * Hours field. Allows: * , / - + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_HoursField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('H'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + // Change timezone to UTC temporarily. This will + // allow us to go back or forwards and hour even + // if DST will be changed between the hours. + $timezone = $date->getTimezone(); + $date->setTimezone(new DateTimeZone('UTC')); + if ($invert) { + $date->modify('-1 hour'); + $date->setTime($date->format('H'), 59); + } else { + $date->modify('+1 hour'); + $date->setTime($date->format('H'), 0); + } + $date->setTimezone($timezone); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php new file mode 100644 index 0000000000..436acf2f56 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php @@ -0,0 +1,39 @@ +<?php + +/** + * Minutes field. Allows: * , / - + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_MinutesField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('i'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 minute'); + } else { + $date->modify('+1 minute'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MonthField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MonthField.php new file mode 100644 index 0000000000..d3deb129f4 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_MonthField.php @@ -0,0 +1,55 @@ +<?php + +/** + * Month field. Allows: * , / - + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_MonthField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + // Convert text month values to integers + $value = str_ireplace( + array( + 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', + 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' + ), + range(1, 12), + $value + ); + + return $this->isSatisfied($date->format('m'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + // $date->modify('last day of previous month'); // remove for php 5.2 compat + $date->modify('previous month'); + $date->modify($date->format('Y-m-t')); + $date->setTime(23, 59); + } else { + //$date->modify('first day of next month'); // remove for php 5.2 compat + $date->modify('next month'); + $date->modify($date->format('Y-m-01')); + $date->setTime(0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_YearField.php b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_YearField.php new file mode 100644 index 0000000000..f11562e451 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/CronExpression_YearField.php @@ -0,0 +1,43 @@ +<?php + +/** + * Year field. Allows: * , / - + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class CronExpression_YearField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('Y'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 year'); + $date->setDate($date->format('Y'), 12, 31); + $date->setTime(23, 59, 0); + } else { + $date->modify('+1 year'); + $date->setDate($date->format('Y'), 1, 1); + $date->setTime(0, 0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/LICENSE b/vendor/woocommerce/action-scheduler/lib/cron-expression/LICENSE new file mode 100644 index 0000000000..c6d88ac6c2 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com> and contributors + +Permission 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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE 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. diff --git a/vendor/woocommerce/action-scheduler/lib/cron-expression/README.md b/vendor/woocommerce/action-scheduler/lib/cron-expression/README.md new file mode 100644 index 0000000000..d4d9d5add1 --- /dev/null +++ b/vendor/woocommerce/action-scheduler/lib/cron-expression/README.md @@ -0,0 +1,92 @@ +PHP Cron Expression Parser +========================== + +[![Latest Stable Version](https://poser.pugx.org/mtdowling/cron-expression/v/stable.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Total Downloads](https://poser.pugx.org/mtdowling/cron-expression/downloads.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Build Status](https://secure.travis-ci.org/mtdowling/cron-expression.png)](http://travis-ci.org/mtdowling/cron-expression) + +The PHP cron expression parser can parse a CRON expression, determine if it is +due to run, calculate the next run date of the expression, and calculate the previous +run date of the expression. You can calculate dates far into the future or past by +skipping n number of matching dates. + +The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), +lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to +find the last day of the month, L to find the last given weekday of a month, and hash +(#) to find the nth weekday of a given month. + +Credits +========== + +Created by Micheal Dowling. Ported to PHP 5.2 by Flightless, Inc. +Based on version 1.0.3: https://github.com/mtdowling/cron-expression/tree/v1.0.3 + +Installing +========== + +Add the following to your project's composer.json: + +```javascript +{ + "require": { + "mtdowling/cron-expression": "1.0.*" + } +} +``` + +Usage +===== +```php +<?php + +require_once '/vendor/autoload.php'; + +// Works with predefined scheduling definitions +$cron = Cron\CronExpression::factory('@daily'); +$cron->isDue(); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); +echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); + +// Works with complex expressions +$cron = Cron\CronExpression::factory('3-59/15 2,6-12 */15 1 2-5'); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); + +// Calculate a run date two iterations into the future +$cron = Cron\CronExpression::factory('@daily'); +echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); + +// Calculate a run date relative to a specific time +$cron = Cron\CronExpression::factory('@monthly'); +echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); +``` + +CRON Expressions +================ + +A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: + + * * * * * * + - - - - - - + | | | | | | + | | | | | + year [optional] + | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + | | | +---------- month (1 - 12) + | | +--------------- day of month (1 - 31) + | +-------------------- hour (0 - 23) + +------------------------- min (0 - 59) + +Requirements +============ + +- PHP 5.3+ +- PHPUnit is required to run the unit tests +- Composer is required to run the unit tests + +CHANGELOG +========= + +1.0.3 (2013-11-23) +------------------ + +* Only set default timezone if the given $currentTime is not a DateTime instance (#34) +* Fixes issue #28 where PHP increments of ranges were failing due to PHP casting hyphens to 0 +* Now supports expressions with any number of extra spaces, tabs, or newlines +* Using static instead of self in `CronExpression::factory` diff --git a/vendor/woocommerce/action-scheduler/license.txt b/vendor/woocommerce/action-scheduler/license.txt new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/vendor/woocommerce/action-scheduler/license.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/vendor/woocommerce/action-scheduler/readme.txt b/vendor/woocommerce/action-scheduler/readme.txt new file mode 100644 index 0000000000..2afd6278cf --- /dev/null +++ b/vendor/woocommerce/action-scheduler/readme.txt @@ -0,0 +1,89 @@ +=== Action Scheduler === +Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 +Tags: scheduler, cron +Requires at least: 5.2 +Tested up to: 5.7 +Stable tag: 3.4.0 +License: GPLv3 +Requires PHP: 5.6 + +Action Scheduler - Job Queue for WordPress + +== Description == + +Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. + +Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions. + +Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. + +## Battle-Tested Background Processing + +Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. + +It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations. + +This is all on infrastructure and WordPress sites outside the control of the plugin author. + +If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. + +## Learn More + +To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org). + +There you will find: + +* [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler +* [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI +* [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions +* [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen +* [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner + +## Credits + +Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). + +Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. + +== Changelog == + += 3.4.0 - 2021-10-29 = +* Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 +* Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 +* Fix - Avoid triggering autoloaders during the version resolution process (props @olegabr). #731 & #776 +* Dev - ActionScheduler_wcSystemStatus PHPCS fixes (props @ovidiul). #761 +* Dev - ActionScheduler_DBLogger.php PHPCS fixes (props @ovidiul). #768 +* Dev - Fixed phpcs for ActionScheduler_Schedule_Deprecated (props @ovidiul). #762 +* Dev - Improve actions table indicies (props @glagonikas). #774 & #777 +* Dev - PHPCS fixes for ActionScheduler_DBStore.php (props @ovidiul). #769 & #778 +* Dev - PHPCS Fixes for ActionScheduler_Abstract_ListTable (props @ovidiul). #763 & #779 +* Dev - Adds new filter action_scheduler_claim_actions_order_by to allow tuning of the claim query (props @glagonikas). #773 +* Dev - PHPCS fixes for ActionScheduler_WpPostStore class (props @ovidiul). #780 + += 3.3.0 - 2021-09-15 = +* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645 +* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519 +* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645 +* Dev - Now supports queries that use multiple statuses. #649 +* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723 + += 3.2.1 - 2021-06-21 = +* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714 +* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600. + += 3.2.0 - 2021-06-03 = +* Fix - Add "no ordering" option to as_next_scheduled_action(). +* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634. +* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634. +* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas). +* Fix - Fix unit tests infrastructure and adapt tests to PHP 8. +* Fix - Identify in-use data store. +* Fix - Improve test_migration_is_scheduled. +* Fix - PHP notice on list table. +* Fix - Speed up clean up and batch selects. +* Fix - Update pending dependencies. +* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array(). +* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility. +* Fix - add is_initialized() to docs. +* Fix - fix file permissions. +* Fix - fixes #664 by replacing __ with esc_html__. diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 8500801d4b..0000000000 --- a/webpack.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Webpack config - * - * @package LifterLMS/Scripts/Dev - * - * @since 5.5.0 - * @version 5.5.0 - */ - -const generate = require( '@lifterlms/scripts/config/webpack.config' ), - config = generate( { - js: [ 'admin-addons' ], - css: [ 'admin-addons' ], - } ); - - -// Remove the directory clearer. -config.plugins = config.plugins.filter( plugin => { - return 'CleanWebpackPlugin' !== plugin.constructor.name; -} ); - -module.exports = config;